diff --git a/.ci.yaml b/.ci.yaml index c00fcf0b7865..bc1feca37966 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -11,25 +11,17 @@ enabled_branches: platform_properties: linux: properties: - caches: >- - [ - ] dependencies: > [ - {"dependency": "curl"} + {"dependency": "curl", "version": "version:7.64.0"} ] device_type: none os: Linux windows: properties: - caches: >- - [ - {"name": "vsbuild", "path": "vsbuild"}, - {"name": "pub_cache", "path": ".pub-cache"} - ] dependencies: > [ - {"dependency": "certs"} + {"dependency": "certs", "version": "version:9563bb"} ] device_type: none os: Windows @@ -45,7 +37,7 @@ targets: version_file: flutter_master.version dependencies: > [ - {"dependency": "vs_build"} + {"dependency": "vs_build", "version": "version:vs2019"} ] - name: Windows win32-platform_tests stable @@ -55,9 +47,10 @@ targets: add_recipes_cq: "true" target_file: windows_build_and_platform_tests.yaml channel: stable + version_file: flutter_stable.version dependencies: > [ - {"dependency": "vs_build"} + {"dependency": "vs_build", "version": "version:vs2019"} ] - name: Windows windows-build_all_plugins master @@ -70,7 +63,7 @@ targets: version_file: flutter_master.version dependencies: > [ - {"dependency": "vs_build"} + {"dependency": "vs_build", "version": "version:vs2019"} ] - name: Windows windows-build_all_plugins stable @@ -80,22 +73,10 @@ targets: add_recipes_cq: "true" target_file: build_all_plugins.yaml channel: stable + version_file: flutter_stable.version dependencies: > [ - {"dependency": "vs_build"} - ] - - - name: Windows uwp-platform_tests master - recipe: plugins/plugins - timeout: 30 - properties: - add_recipes_cq: "true" - target_file: uwp_build_and_platform_tests.yaml - channel: master - version_file: flutter_master.version - dependencies: > - [ - {"dependency": "vs_build"} + {"dependency": "vs_build", "version": "version:vs2019"} ] - name: Windows plugin_tools_tests diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 73efa3103922..bec62f22cc89 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,47 +1,40 @@ # The Flutter version is not important here, since the CI scripts update Flutter # before running. What matters is that the base image is pinned to minimize # unintended changes when modifying this file. -FROM cirrusci/flutter:2.8.0 +# This is the hash for the 3.0.0 image. +FROM cirrusci/flutter@sha256:0224587bba33241cf908184283ec2b544f1b672d87043ead1c00521c368cf844 RUN apt-get update -y +# Set up Firebase Test Lab requirements. RUN apt-get install -y --no-install-recommends gnupg - -# Add repo for gcloud sdk and install it RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | \ sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list - RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | \ sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - - RUN apt-get update && apt-get install -y google-cloud-sdk && \ gcloud config set core/disable_usage_reporting true && \ gcloud config set component_manager/disable_update_check true -RUN yes | sdkmanager \ - "platforms;android-27" \ - "build-tools;27.0.3" \ - "extras;google;m2repository" \ - "extras;android;m2repository" - -RUN yes | sdkmanager --licenses - -# Install formatter. +# Install formatter for C-based languages. RUN apt-get install -y clang-format -# Install xvfb to allow running headless -RUN apt-get install -y xvfb libegl1-mesa -# Install Linux desktop build tool requirements. +# Install Linux desktop requirements: +# - build tools. RUN apt-get install -y clang cmake ninja-build file pkg-config -# Install necessary libraries. +# - libraries. RUN apt-get install -y libgtk-3-dev libblkid-dev liblzma-dev libgcrypt20-dev +# - xvfb to allow running headless. +RUN apt-get install -y xvfb libegl1-mesa -# Add repo for Google Chrome and install it +# Install Chrome and make it the default browser, for url_launcher tests. +# IMPORTANT: Web tests should use a pinned version of Chromium, not this, since +# this isn't pinned, so any time the docker image is re-created the version of +# Chrome may change. RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - RUN echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | sudo tee /etc/apt/sources.list.d/google-chrome.list RUN apt-get update && apt-get install -y --no-install-recommends google-chrome-stable - -# Make Chrome the default so http: and file: has a handler for url_launcher tests. +# Make Chrome the default for http:, https: and file:. RUN apt-get install -y xdg-utils RUN xdg-settings set default-web-browser google-chrome.desktop RUN xdg-mime default google-chrome.desktop inode/directory diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index a76b27a3dd0f..68543c61125f 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -f63e0c8d46826e79f671c9b1e9580ecddcd8f3d7 +ea0ddc94ccc63cec77bcac3ef02832806adcd667 diff --git a/.ci/flutter_stable.version b/.ci/flutter_stable.version new file mode 100644 index 000000000000..55a6a928f5b9 --- /dev/null +++ b/.ci/flutter_stable.version @@ -0,0 +1 @@ +6928314d505d2bb4777be05e45d7808a5aa91d2a diff --git a/.ci/scripts/build_examples_uwp.sh b/.ci/scripts/build_examples_uwp.sh deleted file mode 100644 index 04b8256891bd..000000000000 --- a/.ci/scripts/build_examples_uwp.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/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. - -dart ./script/tool/bin/flutter_plugin_tools.dart build-examples --winuwp \ - --packages-for-branch --log-timing diff --git a/.ci/scripts/prepare_tool.sh b/.ci/scripts/prepare_tool.sh old mode 100644 new mode 100755 index 1095e2189a36..f93694bf1ff6 --- a/.ci/scripts/prepare_tool.sh +++ b/.ci/scripts/prepare_tool.sh @@ -4,7 +4,7 @@ # found in the LICENSE file. # To set FETCH_HEAD for "git merge-base" to work -git fetch origin master +git fetch origin main cd script/tool dart pub get diff --git a/.ci/targets/uwp_build_and_platform_tests.yaml b/.ci/targets/uwp_build_and_platform_tests.yaml deleted file mode 100644 index a7f070776ff1..000000000000 --- a/.ci/targets/uwp_build_and_platform_tests.yaml +++ /dev/null @@ -1,5 +0,0 @@ -tasks: - - name: prepare tool - script: .ci/scripts/prepare_tool.sh - - name: build examples (UWP) - script: .ci/scripts/build_examples_uwp.sh diff --git a/.cirrus.yml b/.cirrus.yml index b283e84f4b91..4a7ec5bb1598 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,25 +1,37 @@ -gcp_credentials: ENCRYPTED[!2c88dee9c9d9805b214c9f7ad8f3bc8fae936cdb0f881d562101151c408c7e024a41222677d5831df90c60d2dd6cd80a!] +gcp_credentials: ENCRYPTED[!f1177d1ddb5330ffaa9ea11c9c9e8e0c542185e895c36071f18cec923dd31c50ece6d18da89c2f6f1cd2d1a98d0c2eea!] -# Don't run on release tags since it creates O(n^2) tasks where n is the -# number of plugins -only_if: $CIRRUS_TAG == '' +# 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: "./script/tool/bin/flutter_plugin_tools.dart" + PLUGIN_TOOL_COMMAND: "dart ./script/tool/bin/flutter_plugin_tools.dart" tool_setup_template: &TOOL_SETUP_TEMPLATE tool_setup_script: - - git fetch origin main # To set FETCH_HEAD for "git merge-base" to work - - cd script/tool - - dart pub get + - .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: - # Master uses a pinned, auto-rolled version to prevent out-of-band CI - # failures due to changes in Flutter. - # TODO(stuartmorgan): Investigate an autoroller for stable as well. + # Channels that are part of our normal test matrix use a pinned, + # auto-rolled version to prevent out-of-band CI failures due to changes in + # Flutter. - TARGET_TREEISH=$CHANNEL - - if [[ "$CHANNEL" == "master" ]]; then + - if [[ "$CHANNEL" == "master" || "$CHANNEL" == "stable" ]]; then - TARGET_TREEISH=$(< .ci/flutter_$CHANNEL.version) - fi # Ensure that the repository has all the branches. @@ -28,10 +40,10 @@ flutter_upgrade_template: &FLUTTER_UPGRADE_TEMPLATE - git fetch origin # Switch to the requested channel. - git checkout $TARGET_TREEISH - # When using a branch rather than a hash, reset to the upstream branch - # rather than using pull, since the base image can sometimes be in a state - # where it has diverged from upstream (!). - - if [[ "$TARGET_TREEISH" == "$CHANNEL" ]]; then + # When using a branch rather than a hash or version tag, reset to the + # upstream branch rather than using pull, since the base image can sometimes + # be in a state where it has diverged from upstream (!). + - if [[ "$TARGET_TREEISH" == "$CHANNEL" ]] && [[ "$CHANNEL" != *"."* ]]; then - git reset --hard @{u} - fi # Run doctor to allow auditing of what version of Flutter the run is using. @@ -40,7 +52,7 @@ flutter_upgrade_template: &FLUTTER_UPGRADE_TEMPLATE build_all_plugins_app_template: &BUILD_ALL_PLUGINS_APP_TEMPLATE create_all_plugins_app_script: - - dart $PLUGIN_TOOL all-plugins-app --output-dir=. --exclude script/configs/exclude_all_plugins_app.yaml + - $PLUGIN_TOOL_COMMAND all-plugins-app --output-dir=. --exclude script/configs/exclude_all_plugins_app.yaml build_all_plugins_debug_script: - cd all_plugins - if [[ "$BUILD_ALL_ARGS" == "web" ]]; then @@ -52,13 +64,6 @@ build_all_plugins_app_template: &BUILD_ALL_PLUGINS_APP_TEMPLATE - cd all_plugins - flutter build $BUILD_ALL_ARGS --release -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' - osx_instance: - image: big-sur-xcode-13 - # Light-workload Linux tasks. # These use default machines, with fewer CPUs, to reduce pressure on the # concurrency limits. @@ -77,42 +82,48 @@ task: script: - cd script/tool - dart pub run test - - name: publishable - env: - # TODO (mvanbeusekom): Temporary override to "stable" because of failure on "master". - # Remove override once https://github.com/dart-lang/pub/issues/3152 is resolved. - CHANNEL: stable - CHANGE_DESC: "$TMPDIR/change-description.txt" - version_check_script: - # For pre-submit, pass the PR description to the script to allow for - # version check overrides. - # For post-submit, ignore platform version breaking version changes and - # missing version/CHANGELOG detection; the PR description isn't reliably - # part of the commit message, so using the same flags as for presubmit - # would likely result in false-positive post-submit failures. - - if [[ $CIRRUS_PR == "" ]]; then - - ./script/tool_runner.sh version-check --ignore-platform-interface-breaks - - else - - echo "$CIRRUS_CHANGE_MESSAGE" > "$CHANGE_DESC" - - ./script/tool_runner.sh version-check --check-for-missing-changes --change-description-file="$CHANGE_DESC" - - fi - publish_check_script: ./script/tool_runner.sh publish-check - - name: format + # Repository rules and best-practice enforcement. + # Only channel-agnostic tests should go here since it is only run once + # (on Flutter master). + - name: repo_checks 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 - license_script: dart $PLUGIN_TOOL license-check - - name: federated_safety - # This check is only meaningful for PRs, as it validates changes - # rather than state. - only_if: $CIRRUS_PR != "" - script: ./script/tool_runner.sh federation-safety-check + readme_script: + - ./script/tool_runner.sh readme-check + # Re-run with --require-excerpts, skipping packages that still need + # to be converted. Once https://github.com/flutter/flutter/issues/102679 + # has been fixed, this can be removed and there can just be a single + # run with --require-excerpts and no exclusions. + - ./script/tool_runner.sh readme-check --require-excerpts --exclude=script/configs/temp_exclude_excerpt.yaml + dependabot_script: $PLUGIN_TOOL_COMMAND dependabot-check + version_script: + # For pre-submit, pass the PR labels to the script to allow for + # check overrides. + # For post-submit, ignore platform version breaking version changes + # and missing version/CHANGELOG detection since the labels aren't + # available outside of the context of the PR. + - if [[ $CIRRUS_PR == "" ]]; then + - ./script/tool_runner.sh version-check --ignore-platform-interface-breaks + - else + - ./script/tool_runner.sh version-check --check-for-missing-changes --pr-labels="$CIRRUS_PR_LABELS" + - fi + publishable_script: ./script/tool_runner.sh publish-check --allow-pre-release + federated_safety_script: + # This check is only meaningful for PRs, as it validates changes + # rather than state. + - if [[ $CIRRUS_PR == "" ]]; then + - ./script/tool_runner.sh federation-safety-check + - else + - echo "Only run in presubmit" + - fi - name: dart_unit_tests env: matrix: CHANNEL: "master" CHANNEL: "stable" - test_script: + unit_test_script: - ./script/tool_runner.sh test - name: analyze env: @@ -134,10 +145,46 @@ task: # This uses --run-on-dirty-packages rather than --packages-for-branch # since only the packages changed by 'make-deps-path-based' need to be # checked. - - dart $PLUGIN_TOOL analyze --run-on-dirty-packages --log-timing --custom-analysis=script/configs/custom_analysis.yaml + - $PLUGIN_TOOL_COMMAND analyze --run-on-dirty-packages --log-timing --custom-analysis=script/configs/custom_analysis.yaml # Restore the tree to a clean state, to avoid accidental issues if # other script steps are added to this task. - git checkout . + # Does a sanity check that packages at least pass analysis on the N-1 and N-2 + # versions of Flutter stable if the package claims to support that version. + # This is to minimize accidentally making changes that break old versions + # (which we don't commit to supporting, but don't want to actively break) + # without updating the constraints. + # Note: The versions below should be manually updated after a new stable + # version comes out. + - name: legacy_version_analyze + depends_on: analyze + matrix: + env: + CHANNEL: "3.0.5" + DART_VERSION: "2.17.6" + env: + CHANNEL: "2.10.5" + DART_VERSION: "2.16.2" + package_prep_script: + # Allow analyzing packages that use a dev dependency with a higher + # minimum Flutter/Dart version than the package itself. + - ./script/tool_runner.sh remove-dev-dependencies + analyze_script: + # Only analyze lib/; non-client code doesn't need to work on + # all supported legacy version. + - ./script/tool_runner.sh analyze --lib-only --skip-if-not-supporting-flutter-version="$CHANNEL" --skip-if-not-supporting-dart-version="$DART_VERSION" --custom-analysis=script/configs/custom_analysis.yaml + # Does a sanity check that packages pass analysis with the lowest possible + # versions of all dependencies. This is to catch cases where we add use of + # new APIs but forget to update minimum versions of dependencies to where + # those APIs are introduced. + - name: downgraded_analyze + depends_on: analyze + analyze_script: + - ./script/tool_runner.sh analyze --downgrade --custom-analysis=script/configs/custom_analysis.yaml + - name: readme_excerpts + env: + CIRRUS_CLONE_SUBMODULES: true + script: ./script/tool_runner.sh update-excerpts --fail-on-change ### Web tasks ### - name: web-build_all_plugins env: @@ -153,21 +200,20 @@ task: matrix: CHANNEL: "master" CHANNEL: "stable" - setup_script: - - flutter config --enable-linux-desktop << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - name: linux-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" build_script: - - flutter config --enable-linux-desktop - ./script/tool_runner.sh build-examples --linux native_test_script: - - ./script/tool_runner.sh native-test --linux --no-integration + - xvfb-run ./script/tool_runner.sh native-test --linux --no-integration drive_script: - - xvfb-run ./script/tool_runner.sh drive-examples --linux + - xvfb-run ./script/tool_runner.sh drive-examples --linux --exclude=script/configs/exclude_integration_linux.yaml # Heavy-workload Linux tasks. # These use machines with more CPUs and memory, so will reduce parallelization @@ -186,48 +232,30 @@ task: matrix: ### Android tasks ### - name: android-platform_tests + # Don't run full platform tests on both channels in pre-submit. + skip: $CIRRUS_PR != '' && $CHANNEL == 'stable' env: matrix: - PLUGIN_SHARDING: "--shardIndex 0 --shardCount 5" - PLUGIN_SHARDING: "--shardIndex 1 --shardCount 5" - PLUGIN_SHARDING: "--shardIndex 2 --shardCount 5" - PLUGIN_SHARDING: "--shardIndex 3 --shardCount 5" - PLUGIN_SHARDING: "--shardIndex 4 --shardCount 5" + PACKAGE_SHARDING: "--shardIndex 0 --shardCount 5" + PACKAGE_SHARDING: "--shardIndex 1 --shardCount 5" + PACKAGE_SHARDING: "--shardIndex 2 --shardCount 5" + PACKAGE_SHARDING: "--shardIndex 3 --shardCount 5" + PACKAGE_SHARDING: "--shardIndex 4 --shardCount 5" matrix: CHANNEL: "master" CHANNEL: "stable" MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] - GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[de02374f8d2d14d50792c6b521af2dfb86cbb522efed104f905002e4332546104d387d2bb8710956b729b4bd6533bba0] + GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[30e6cf7189e3ff3868edc25d2e638ef2aec70546456427064bbc74b297d36145364f49f9d26b327787a59df149d69262] build_script: - # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they - # might include non-ASCII characters which makes Gradle crash. - # TODO(stuartmorgan): See https://github.com/flutter/flutter/issues/24935 - - export CIRRUS_CHANGE_MESSAGE="" - - export CIRRUS_COMMIT_MESSAGE="" - ./script/tool_runner.sh build-examples --apk lint_script: - # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they - # might include non-ASCII characters which makes Gradle crash. - # TODO(stuartmorgan): See https://github.com/flutter/flutter/issues/24935 - - export CIRRUS_CHANGE_MESSAGE="" - - export CIRRUS_COMMIT_MESSAGE="" - ./script/tool_runner.sh lint-android # must come after build-examples native_unit_test_script: - # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they - # might include non-ASCII characters which makes Gradle crash. - # TODO(stuartmorgan): See https://github.com/flutter/flutter/issues/24935 - - export CIRRUS_CHANGE_MESSAGE="" - - export CIRRUS_COMMIT_MESSAGE="" # 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 firebase_test_lab_script: - # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they - # might include non-ASCII characters which makes Gradle crash. - # TODO(stuartmorgan): See https://github.com/flutter/flutter/issues/24935 - - export CIRRUS_CHANGE_MESSAGE="" - - export CIRRUS_COMMIT_MESSAGE="" - if [[ -n "$GCLOUD_FIREBASE_TESTLAB_KEY" ]]; then - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json - ./script/tool_runner.sh firebase-test-lab --device model=redfin,version=30 --device model=starqlteue,version=26 --exclude=script/configs/exclude_integration_android.yaml @@ -251,8 +279,8 @@ task: - name: web-platform_tests env: matrix: - PLUGIN_SHARDING: "--shardIndex 0 --shardCount 2" - PLUGIN_SHARDING: "--shardIndex 1 --shardCount 2" + PACKAGE_SHARDING: "--shardIndex 0 --shardCount 2" + PACKAGE_SHARDING: "--shardIndex 1 --shardCount 2" matrix: CHANNEL: "master" CHANNEL: "stable" @@ -271,15 +299,11 @@ task: drive_script: - ./script/tool_runner.sh drive-examples --web --exclude=script/configs/exclude_integration_web.yaml -# macOS tasks. +# ARM macOS tasks. task: - << : *MACOS_TEMPLATE + << : *MACOS_ARM_TEMPLATE << : *FLUTTER_UPGRADE_TEMPLATE matrix: - ### iOS+macOS tasks *** - - name: darwin-lint_podspecs - script: - - ./script/tool_runner.sh podspecs ### iOS tasks ### - name: ios-build_all_plugins env: @@ -288,14 +312,53 @@ task: 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 + +# Intel macOS tasks. +task: + << : *MACOS_INTEL_TEMPLATE + << : *FLUTTER_UPGRADE_TEMPLATE + matrix: + ### iOS+macOS tasks *** + # TODO(stuartmorgan): Move this to ARM once google_maps_flutter has ARM + # support. `pod lint` makes a synthetic target that doesn't respect the + # pod's arch exclusions, so fails to build. + - name: darwin-lint_podspecs + script: + - ./script/tool_runner.sh podspecs + ### 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: ios-platform_tests + # Don't run full platform tests on both channels in pre-submit. + skip: $CIRRUS_PR != '' && $CHANNEL == 'stable' env: PATH: $PATH:/usr/local/bin matrix: - PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" - PLUGIN_SHARDING: "--shardIndex 1 --shardCount 4" - PLUGIN_SHARDING: "--shardIndex 2 --shardCount 4" - PLUGIN_SHARDING: "--shardIndex 3 --shardCount 4" + PACKAGE_SHARDING: "--shardIndex 0 --shardCount 4" + PACKAGE_SHARDING: "--shardIndex 1 --shardCount 4" + PACKAGE_SHARDING: "--shardIndex 2 --shardCount 4" + PACKAGE_SHARDING: "--shardIndex 3 --shardCount 4" matrix: CHANNEL: "master" CHANNEL: "stable" @@ -307,6 +370,9 @@ task: - ./script/tool_runner.sh build-examples --ios xcode_analyze_script: - ./script/tool_runner.sh xcode-analyze --ios + xcode_analyze_deprecation_script: + # Ensure we don't accidentally introduce deprecated code. + - ./script/tool_runner.sh xcode-analyze --ios --ios-min-version=13.0 native_test_script: - ./script/tool_runner.sh native-test --ios --ios-destination "platform=iOS Simulator,name=iPhone 11,OS=latest" drive_script: @@ -315,6 +381,8 @@ task: # So we run `drive-examples` after `native-test`; changing the order will result ci failure. - ./script/tool_runner.sh drive-examples --ios --exclude=script/configs/exclude_integration_ios.yaml ### macOS desktop tasks ### + # macos-platform_tests builds all the plugins on M1, so this build is run + # on Intel to give us build coverage of both host types. - name: macos-build_all_plugins env: BUILD_ALL_ARGS: "macos" @@ -322,20 +390,4 @@ task: CHANNEL: "master" CHANNEL: "stable" setup_script: - - flutter config --enable-macos-desktop << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - - name: macos-platform_tests - env: - matrix: - CHANNEL: "master" - CHANNEL: "stable" - PATH: $PATH:/usr/local/bin - build_script: - - flutter config --enable-macos-desktop - - ./script/tool_runner.sh build-examples --macos - xcode_analyze_script: - - ./script/tool_runner.sh xcode-analyze --macos - native_test_script: - - ./script/tool_runner.sh native-test --macos - drive_script: - - ./script/tool_runner.sh drive-examples --macos diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e19d8e1d80cd..9fe5a37a4fa8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -21,9 +21,9 @@ If you need help, consider asking for advice on the #hackers-new channel on [Discord]. -[Contributor Guide]: https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md +[Contributor Guide]: https://github.com/flutter/plugins/blob/main/CONTRIBUTING.md [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene -[relevant style guides]: https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md#style +[relevant style guides]: https://github.com/flutter/plugins/blob/main/CONTRIBUTING.md#style [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes @@ -31,5 +31,5 @@ If you need help, consider asking for advice on the #hackers-new channel on [Dis [pub versioning philosophy]: https://dart.dev/tools/pub/versioning [exempt from version changes]: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#version-and-changelog-updates [following repository CHANGELOG style]: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changelog-style -[the auto-formatter]: https://github.com/flutter/plugins/blob/master/script/tool/README.md#format-code +[the auto-formatter]: https://github.com/flutter/plugins/blob/main/script/tool/README.md#format-code [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..16346f9a0b8c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,591 @@ +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/packages/camera/camera_android/android" + commit-message: + prefix: "[camera]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/camera/camera_android/example/android/app" + commit-message: + prefix: "[camera]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/camera/camera_android_camerax/android" + commit-message: + prefix: "[camera]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/camera/camera_android_camerax/example/android/app" + commit-message: + prefix: "[camera]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/camera/camera/example/android/app" + commit-message: + prefix: "[camera]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/espresso/android" + commit-message: + prefix: "[espresso]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/espresso/example/android/app" + commit-message: + prefix: "[espresso]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/flutter_plugin_android_lifecycle/android" + commit-message: + prefix: "[lifecycle]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/flutter_plugin_android_lifecycle/example/android/app" + commit-message: + prefix: "[lifecycle]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/google_maps_flutter/google_maps_flutter/example/android/app" + commit-message: + prefix: "[google_maps]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/google_maps_flutter/google_maps_flutter_android/android" + commit-message: + prefix: "[google_maps]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/google_maps_flutter/google_maps_flutter_android/example/android/app" + commit-message: + prefix: "[google_maps]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/google_sign_in/google_sign_in/example/android/app" + commit-message: + prefix: "[sign_in]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/google_sign_in/google_sign_in_android/android" + commit-message: + prefix: "[sign_in]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/google_sign_in/google_sign_in_android/example/android/app" + commit-message: + prefix: "[sign_in]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/in_app_purchase/in_app_purchase_android/android" + commit-message: + prefix: "[in_app_pur]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/in_app_purchase/in_app_purchase_android/example/android/app" + commit-message: + prefix: "[in_app_pur]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/in_app_purchase/in_app_purchase/example/android/app" + commit-message: + prefix: "[in_app_pur]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/image_picker/image_picker/example/android/app" + commit-message: + prefix: "[image_picker]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/image_picker/image_picker_android/android" + commit-message: + prefix: "[image_picker]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/image_picker/image_picker_android/example/android/app" + commit-message: + prefix: "[image_picker]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/local_auth/local_auth_android/android" + commit-message: + prefix: "[local_auth]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/local_auth/local_auth_android/example/android/app" + commit-message: + prefix: "[local_auth]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/local_auth/local_auth/example/android/app" + commit-message: + prefix: "[local_auth]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/path_provider/path_provider/example/android/app" + commit-message: + prefix: "[path_provider]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/path_provider/path_provider_android/android" + commit-message: + prefix: "[path_provider]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/path_provider/path_provider_android/example/android/app" + commit-message: + prefix: "[path_provider]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/quick_actions/quick_actions_android/android" + commit-message: + prefix: "[quick_actions]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/quick_actions/quick_actions_android/example/android/app" + commit-message: + prefix: "[quick_actions]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/quick_actions/quick_actions/example/android/app" + commit-message: + prefix: "[quick_actions]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/shared_preferences/shared_preferences/example/android/app" + commit-message: + prefix: "[shared_pref]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/shared_preferences/shared_preferences_android/android" + commit-message: + prefix: "[shared_pref]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/shared_preferences/shared_preferences_android/example/android/app" + commit-message: + prefix: "[shared_pref]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/url_launcher/url_launcher_android/android" + commit-message: + prefix: "[url_launcher]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/url_launcher/url_launcher_android/example/android/app" + commit-message: + prefix: "[url_launcher]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/url_launcher/url_launcher/example/android/app" + commit-message: + prefix: "[url_launcher]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/video_player/video_player/example/android/app" + commit-message: + prefix: "[video_player]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/video_player/video_player_android/android" + commit-message: + prefix: "[video_player]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/video_player/video_player_android/example/android/app" + commit-message: + prefix: "[video_player]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/webview_flutter/webview_flutter/example/android/app" + commit-message: + prefix: "[webview]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/webview_flutter/webview_flutter_android/android" + commit-message: + prefix: "[webview]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "/packages/webview_flutter/webview_flutter_android/example/android/app" + commit-message: + prefix: "[webview]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "github-actions" + directory: "/" + commit-message: + prefix: "[gh_actions]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml deleted file mode 100644 index 9026643d7b2e..000000000000 --- a/.github/workflows/mirror.yml +++ /dev/null @@ -1,25 +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. - -# Mirror master to main branches in the plugins repository. -on: - push: - branches: - - 'main' - -jobs: - mirror_job: - permissions: - pull-requests: write - runs-on: ubuntu-latest - if: ${{ github.repository == 'flutter/plugins' }} - name: Mirror main branch to master branch - steps: - - name: Mirror action step - id: mirror - uses: google/mirror-branch-action@c6b07e441a7ffc5ae15860c1d0a8107a3a151db8 - with: - github-token: ${{ secrets.FLUTTERMIRRORINGBOT_TOKEN }} - source: 'main' - dest: 'master' diff --git a/.github/workflows/pull_request_label.yml b/.github/workflows/pull_request_label.yml index 825a3afd8508..16ad0a3c171a 100644 --- a/.github/workflows/pull_request_label.yml +++ b/.github/workflows/pull_request_label.yml @@ -12,13 +12,16 @@ on: pull_request_target: types: [opened, synchronize, reopened, closed] +# Declare default permissions as read only. +permissions: read-all + jobs: label: permissions: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@9794b1493b6f1fa7b006c5f8635a19c76c98be95 + - uses: actions/labeler@5c7539237e04b714afd8ad9b4aed733815b9fab4 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" sync-labels: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1bf50a0d1dfc..292db55dede7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,9 @@ on: branches: - main +# Declare default permissions as read only. +permissions: read-all + jobs: release: if: github.repository_owner == 'flutter' @@ -24,7 +27,7 @@ jobs: cd $GITHUB_WORKSPACE # Checks out a copy of the repo. - name: Check out code - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 with: fetch-depth: 0 # Fetch all history so the tool can get all the tags to determine version. - name: Set up tools @@ -33,7 +36,7 @@ jobs: # This workflow should be the last to run. So wait for all the other tests to succeed. - name: Wait on all tests - uses: lewagon/wait-on-check-action@5e937358caba2c7876a2ee06e4a48d0664fe4967 + uses: lewagon/wait-on-check-action@3a563271c3f8d1611ed7352809303617ee7e54ac with: ref: ${{ github.sha }} running-workflow-name: 'release' @@ -47,5 +50,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-plugin --all-changed --base-sha=HEAD~ --skip-confirmation --remote=origin + dart ./script/tool/lib/src/main.dart 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 5148f2e19028..55fa7f14591e 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -17,15 +17,17 @@ jobs: security-events: write actions: read contents: read + # Needed to access OIDC token. + id-token: write steps: - name: "Checkout code" - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@b614d455ee90608b5e36e3299cd50d457eb37d5f # v1.0.3 + uses: ossf/scorecard-action@e363bfca00e752f91de7b7d2a77340e2e523cb18 with: results_file: results.sarif results_format: sarif @@ -40,7 +42,7 @@ jobs: # Upload the results as artifacts (optional). - name: "Upload artifact" - uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2 # v2.3.1 + uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb with: name: SARIF file path: results.sarif @@ -48,6 +50,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # v1.0.26 + uses: github/codeql-action/upload-sarif@807578363a7869ca324a79039e6db9c843e0e100 with: sarif_file: results.sarif diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..1d3bb5da1bfb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "site-shared"] + path = site-shared + url = https://github.com/dart-lang/site-shared diff --git a/AUTHORS b/AUTHORS index f5dc82340bc8..3112c3b3fd05 100644 --- a/AUTHORS +++ b/AUTHORS @@ -66,3 +66,6 @@ Alex Li Rahul Raj <64.rahulraj@gmail.com> Daniel Roek TheOneWithTheBraid +Rulong Chen(陈汝龙) +Hwanseok Kang +Twin Sun, LLC diff --git a/CODEOWNERS b/CODEOWNERS index 88ba1f575a4c..04cc2b1c4468 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -5,8 +5,74 @@ # reviewed by someone else. # Plugin-level rules. -packages/webview_flutter/** @bparrishMines +packages/camera/** @bparrishMines +packages/file_selector/** @stuartmorgan +packages/google_maps_flutter/** @stuartmorgan +packages/google_sign_in/** @stuartmorgan +packages/image_picker/** @stuartmorgan +packages/in_app_purchase/** @bparrishMines +packages/local_auth/** @stuartmorgan +packages/path_provider/** @gaaclarke +packages/plugin_platform_interface/** @stuartmorgan +packages/quick_actions/** @stuartmorgan +packages/shared_preferences/** @tarrinneal +packages/url_launcher/** @stuartmorgan +packages/video_player/** @gaaclarke +packages/webview_flutter/** @bparrishMines # Sub-package-level rules. These should stay last, since the last matching # entry takes precedence. -packages/**/*_web/** @ditman + +# - Web +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/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/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/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/image_picker/image_picker_ios/** @cyanglaz +packages/in_app_purchase/in_app_purchase_storekit/** @cyanglaz +packages/ios_platform_images/ios/** @jmagman +packages/local_auth/local_auth_ios/** @hellohuanlin +packages/path_provider/path_provider_ios/** @jmagman +packages/quick_actions/quick_actions_ios/** @hellohuanlin +packages/shared_preferences/shared_preferences_ios/** @cyanglaz +packages/url_launcher/url_launcher_ios/** @jmagman +packages/video_player/video_player_avfoundation/** @hellohuanlin +packages/webview_flutter/webview_flutter_wkwebview/** @cyanglaz + +# - Linux +packages/file_selector/file_selector_linux/** @cbracken +packages/path_provider/path_provider_linux/** @cbracken +packages/shared_preferences/shared_preferences_linux/** @cbracken +packages/url_launcher/url_launcher_linux/** @cbracken + +# - macOS +packages/file_selector/file_selector_macos/** @cbracken +packages/path_provider/path_provider_macos/** @cbracken +packages/shared_preferences/shared_preferences_macos/** @cbracken +packages/url_launcher/url_launcher_macos/** @cbracken + +# - Windows +packages/camera/camera_windows/** @cbracken +packages/file_selector/file_selector_windows/** @cbracken +packages/image_picker/image_picker_windows/** @cbracken +packages/local_auth/local_auth_windows/** @cbracken +packages/path_provider/path_provider_windows/** @cbracken +packages/shared_preferences/shared_preferences_windows/** @cbracken +packages/url_launcher/url_launcher_windows/** @cbracken diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52518202fa84..c2d44d50049b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Flutter Plugins -[![Build Status](https://api.cirrus-ci.com/github/flutter/plugins.svg)](https://cirrus-ci.com/github/flutter/plugins/master) +[![Build Status](https://api.cirrus-ci.com/github/flutter/plugins.svg)](https://cirrus-ci.com/github/flutter/plugins/main) _See also: [Flutter's code of conduct](https://github.com/flutter/flutter/blob/master/CODE_OF_CONDUCT.md)_ @@ -35,7 +35,7 @@ use, and use auto-formatters: - [C++](https://google.github.io/styleguide/cppguide.html) formatted with `clang-format` - **Note**: The Linux plugins generally follow idiomatic GObject-based C style. See [the engine style - notes](https://github.com/flutter/engine/blob/master/CONTRIBUTING.md#style) + notes](https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style) for more details, and exceptions. - [Java](https://google.github.io/styleguide/javaguide.html) formatted with `google-java-format` diff --git a/README.md b/README.md index 30c6a2f00e66..92098af809e9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Flutter plugins -[![Build Status](https://api.cirrus-ci.com/github/flutter/plugins.svg)](https://cirrus-ci.com/github/flutter/plugins/master) +[![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) This repo is a companion repo to the main [flutter repo](https://github.com/flutter/flutter). It contains the source code for @@ -34,28 +35,28 @@ is ready, you can [publish](https://flutter.dev/developing-packages/#publish) it to the [pub repository](https://pub.dev/). If you wish to contribute a change to any of the existing plugins in this repo, -please review our [contribution guide](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md), +please review our [contribution guide](https://github.com/flutter/plugins/blob/main/CONTRIBUTING.md), and send a [pull request](https://github.com/flutter/plugins/pulls). ## Plugins These are the available plugins in this repository. -| Plugin | Pub | Points | Popularity | Likes | -|--------|-----|--------|------------|-------| -| [camera](./packages/camera/) | [![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dev/packages/camera) | [![pub points](https://badges.bar/camera/pub%20points)](https://pub.dev/packages/camera/score) | [![popularity](https://badges.bar/camera/popularity)](https://pub.dev/packages/camera/score) | [![likes](https://badges.bar/camera/likes)](https://pub.dev/packages/camera/score) | -| [espresso](./packages/espresso/) | [![pub package](https://img.shields.io/pub/v/espresso.svg)](https://pub.dev/packages/espresso) | [![pub points](https://badges.bar/espresso/pub%20points)](https://pub.dev/packages/espresso/score) | [![popularity](https://badges.bar/espresso/popularity)](https://pub.dev/packages/espresso/score) | [![likes](https://badges.bar/espresso/likes)](https://pub.dev/packages/espresso/score) | -| [file_selector](./packages/file_selector/) | [![pub package](https://img.shields.io/pub/v/file_selector.svg)](https://pub.dev/packages/file_selector) | [![pub points](https://badges.bar/file_selector/pub%20points)](https://pub.dev/packages/file_selector/score) | [![popularity](https://badges.bar/file_selector/popularity)](https://pub.dev/packages/file_selector/score) | [![likes](https://badges.bar/file_selector/likes)](https://pub.dev/packages/file_selector/score) | -| [flutter_plugin_android_lifecycle](./packages/flutter_plugin_android_lifecycle/) | [![pub package](https://img.shields.io/pub/v/flutter_plugin_android_lifecycle.svg)](https://pub.dev/packages/flutter_plugin_android_lifecycle) | [![pub points](https://badges.bar/flutter_plugin_android_lifecycle/pub%20points)](https://pub.dev/packages/flutter_plugin_android_lifecycle/score) | [![popularity](https://badges.bar/flutter_plugin_android_lifecycle/popularity)](https://pub.dev/packages/flutter_plugin_android_lifecycle/score) | [![likes](https://badges.bar/flutter_plugin_android_lifecycle/likes)](https://pub.dev/packages/flutter_plugin_android_lifecycle/score) | -| [google_maps_flutter](./packages/google_maps_flutter) | [![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dev/packages/google_maps_flutter) | [![pub points](https://badges.bar/google_maps_flutter/pub%20points)](https://pub.dev/packages/google_maps_flutter/score) | [![popularity](https://badges.bar/google_maps_flutter/popularity)](https://pub.dev/packages/google_maps_flutter/score) | [![likes](https://badges.bar/google_maps_flutter/likes)](https://pub.dev/packages/google_maps_flutter/score) | -| [google_sign_in](./packages/google_sign_in/) | [![pub package](https://img.shields.io/pub/v/google_sign_in.svg)](https://pub.dev/packages/google_sign_in) | [![pub points](https://badges.bar/google_sign_in/pub%20points)](https://pub.dev/packages/google_sign_in/score) | [![popularity](https://badges.bar/google_sign_in/popularity)](https://pub.dev/packages/google_sign_in/score) | [![likes](https://badges.bar/google_sign_in/likes)](https://pub.dev/packages/google_sign_in/score) | -| [image_picker](./packages/image_picker/) | [![pub package](https://img.shields.io/pub/v/image_picker.svg)](https://pub.dev/packages/image_picker) | [![pub points](https://badges.bar/image_picker/pub%20points)](https://pub.dev/packages/image_picker/score) | [![popularity](https://badges.bar/image_picker/popularity)](https://pub.dev/packages/image_picker/score) | [![likes](https://badges.bar/image_picker/likes)](https://pub.dev/packages/image_picker/score) | -| [in_app_purchase](./packages/in_app_purchase/) | [![pub package](https://img.shields.io/pub/v/in_app_purchase.svg)](https://pub.dev/packages/in_app_purchase) | [![pub points](https://badges.bar/in_app_purchase/pub%20points)](https://pub.dev/packages/in_app_purchase/score) | [![popularity](https://badges.bar/in_app_purchase/popularity)](https://pub.dev/packages/in_app_purchase/score) | [![likes](https://badges.bar/in_app_purchase/likes)](https://pub.dev/packages/in_app_purchase/score) | -| [ios_platform_images](./packages/ios_platform_images/) | [![pub package](https://img.shields.io/pub/v/ios_platform_images.svg)](https://pub.dev/packages/ios_platform_images) | [![pub points](https://badges.bar/ios_platform_images/pub%20points)](https://pub.dev/packages/ios_platform_images/score) | [![popularity](https://badges.bar/ios_platform_images/popularity)](https://pub.dev/packages/ios_platform_images/score) | [![likes](https://badges.bar/ios_platform_images/likes)](https://pub.dev/packages/ios_platform_images/score) | -| [local_auth](./packages/local_auth/) | [![pub package](https://img.shields.io/pub/v/local_auth.svg)](https://pub.dev/packages/local_auth) | [![pub points](https://badges.bar/local_auth/pub%20points)](https://pub.dev/packages/local_auth/score) | [![popularity](https://badges.bar/local_auth/popularity)](https://pub.dev/packages/local_auth/score) | [![likes](https://badges.bar/local_auth/likes)](https://pub.dev/packages/local_auth/score) | -| [path_provider](./packages/path_provider/) | [![pub package](https://img.shields.io/pub/v/path_provider.svg)](https://pub.dev/packages/path_provider) | [![pub points](https://badges.bar/path_provider/pub%20points)](https://pub.dev/packages/path_provider/score) | [![popularity](https://badges.bar/path_provider/popularity)](https://pub.dev/packages/path_provider/score) | [![likes](https://badges.bar/path_provider/likes)](https://pub.dev/packages/path_provider/score) | -| [plugin_platform_interface](./packages/plugin_platform_interface/) | [![pub package](https://img.shields.io/pub/v/plugin_platform_interface.svg)](https://pub.dev/packages/plugin_platform_interface) | [![pub points](https://badges.bar/plugin_platform_interface/pub%20points)](https://pub.dev/packages/plugin_platform_interface/score) | [![popularity](https://badges.bar/plugin_platform_interface/popularity)](https://pub.dev/packages/plugin_platform_interface/score) | [![likes](https://badges.bar/plugin_platform_interface/likes)](https://pub.dev/packages/plugin_platform_interface/score) | -| [quick_actions](./packages/quick_actions/) | [![pub package](https://img.shields.io/pub/v/quick_actions.svg)](https://pub.dev/packages/quick_actions) | [![pub points](https://badges.bar/quick_actions/pub%20points)](https://pub.dev/packages/quick_actions/score) | [![popularity](https://badges.bar/quick_actions/popularity)](https://pub.dev/packages/quick_actions/score) | [![likes](https://badges.bar/quick_actions/likes)](https://pub.dev/packages/quick_actions/score) | -| [shared_preferences](./packages/shared_preferences/) | [![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dev/packages/shared_preferences) | [![pub points](https://badges.bar/shared_preferences/pub%20points)](https://pub.dev/packages/shared_preferences/score) | [![popularity](https://badges.bar/shared_preferences/popularity)](https://pub.dev/packages/shared_preferences/score) | [![likes](https://badges.bar/shared_preferences/likes)](https://pub.dev/packages/shared_preferences/score) | -| [url_launcher](./packages/url_launcher/) | [![pub package](https://img.shields.io/pub/v/url_launcher.svg)](https://pub.dev/packages/url_launcher) | [![pub points](https://badges.bar/url_launcher/pub%20points)](https://pub.dev/packages/url_launcher/score) | [![popularity](https://badges.bar/url_launcher/popularity)](https://pub.dev/packages/url_launcher/score) | [![likes](https://badges.bar/url_launcher/likes)](https://pub.dev/packages/url_launcher/score) | -| [video_player](./packages/video_player/) | [![pub package](https://img.shields.io/pub/v/video_player.svg)](https://pub.dev/packages/video_player) | [![pub points](https://badges.bar/video_player/pub%20points)](https://pub.dev/packages/video_player/score) | [![popularity](https://badges.bar/video_player/popularity)](https://pub.dev/packages/video_player/score) | [![likes](https://badges.bar/video_player/likes)](https://pub.dev/packages/video_player/score) | -| [webview_flutter](./packages/webview_flutter/) | [![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dev/packages/webview_flutter) | [![pub points](https://badges.bar/webview_flutter/pub%20points)](https://pub.dev/packages/webview_flutter/score) | [![popularity](https://badges.bar/webview_flutter/popularity)](https://pub.dev/packages/webview_flutter/score) | [![likes](https://badges.bar/webview_flutter/likes)](https://pub.dev/packages/webview_flutter/score) | +| Plugin | Pub | Points | Popularity | Likes | Issues | Pull requests | +|--------|-----|--------|------------|-------|--------|---------------| +| [camera](./packages/camera/) | [![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dev/packages/camera) | [![pub points](https://img.shields.io/pub/points/camera)](https://pub.dev/packages/camera/score) | [![popularity](https://img.shields.io/pub/popularity/camera)](https://pub.dev/packages/camera/score) | [![likes](https://img.shields.io/pub/likes/camera)](https://pub.dev/packages/camera/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20camera?label=)](https://github.com/flutter/flutter/labels/p%3A%20camera) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20camera?label=)](https://github.com/flutter/plugins/labels/p%3A%20camera) | +| [espresso](./packages/espresso/) | [![pub package](https://img.shields.io/pub/v/espresso.svg)](https://pub.dev/packages/espresso) | [![pub points](https://img.shields.io/pub/points/espresso)](https://pub.dev/packages/espresso/score) | [![popularity](https://img.shields.io/pub/popularity/espresso)](https://pub.dev/packages/espresso/score) | [![likes](https://img.shields.io/pub/likes/espresso)](https://pub.dev/packages/espresso/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20espresso?label=)](https://github.com/flutter/flutter/labels/p%3A%20espresso) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20espresso?label=)](https://github.com/flutter/plugins/labels/p%3A%20espresso) | +| [file_selector](./packages/file_selector/) | [![pub package](https://img.shields.io/pub/v/file_selector.svg)](https://pub.dev/packages/file_selector) | [![pub points](https://img.shields.io/pub/points/file_selector)](https://pub.dev/packages/file_selector/score) | [![popularity](https://img.shields.io/pub/popularity/file_selector)](https://pub.dev/packages/file_selector/score) | [![likes](https://img.shields.io/pub/likes/file_selector)](https://pub.dev/packages/file_selector/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20file_selector?label=)](https://github.com/flutter/flutter/labels/p%3A%20file_selector) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20file_selector?label=)](https://github.com/flutter/plugins/labels/p%3A%20file_selector) | +| [flutter_plugin_android_lifecycle](./packages/flutter_plugin_android_lifecycle/) | [![pub package](https://img.shields.io/pub/v/flutter_plugin_android_lifecycle.svg)](https://pub.dev/packages/flutter_plugin_android_lifecycle) | [![pub points](https://img.shields.io/pub/points/flutter_plugin_android_lifecycle)](https://pub.dev/packages/flutter_plugin_android_lifecycle/score) | [![popularity](https://img.shields.io/pub/popularity/flutter_plugin_android_lifecycle)](https://pub.dev/packages/flutter_plugin_android_lifecycle/score) | [![likes](https://img.shields.io/pub/likes/flutter_plugin_android_lifecycle)](https://pub.dev/packages/flutter_plugin_android_lifecycle/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20flutter_plugin_android_lifecycle?label=)](https://github.com/flutter/flutter/labels/p%3A%20flutter_plugin_android_lifecycle) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20flutter_plugin_android_lifecycle?label=)](https://github.com/flutter/plugins/labels/p%3A%20flutter_plugin_android_lifecycle) | +| [google_maps_flutter](./packages/google_maps_flutter) | [![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dev/packages/google_maps_flutter) | [![pub points](https://img.shields.io/pub/points/google_maps_flutter)](https://pub.dev/packages/google_maps_flutter/score) | [![popularity](https://img.shields.io/pub/popularity/google_maps_flutter)](https://pub.dev/packages/google_maps_flutter/score) | [![likes](https://img.shields.io/pub/likes/google_maps_flutter)](https://pub.dev/packages/google_maps_flutter/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20maps?label=)](https://github.com/flutter/flutter/labels/p%3A%20maps) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20google_maps_flutter?label=)](https://github.com/flutter/plugins/labels/p%3A%20google_maps_flutter) | +| [google_sign_in](./packages/google_sign_in/) | [![pub package](https://img.shields.io/pub/v/google_sign_in.svg)](https://pub.dev/packages/google_sign_in) | [![pub points](https://img.shields.io/pub/points/google_sign_in)](https://pub.dev/packages/google_sign_in/score) | [![popularity](https://img.shields.io/pub/popularity/google_sign_in)](https://pub.dev/packages/google_sign_in/score) | [![likes](https://img.shields.io/pub/likes/google_sign_in)](https://pub.dev/packages/google_sign_in/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20google_sign_in?label=)](https://github.com/flutter/flutter/labels/p%3A%20google_sign_in) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20google_sign_in?label=)](https://github.com/flutter/plugins/labels/p%3A%20google_sign_in) | +| [image_picker](./packages/image_picker/) | [![pub package](https://img.shields.io/pub/v/image_picker.svg)](https://pub.dev/packages/image_picker) | [![pub points](https://img.shields.io/pub/points/image_picker)](https://pub.dev/packages/image_picker/score) | [![popularity](https://img.shields.io/pub/popularity/image_picker)](https://pub.dev/packages/image_picker/score) | [![likes](https://img.shields.io/pub/likes/image_picker)](https://pub.dev/packages/image_picker/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20image_picker?label=)](https://github.com/flutter/flutter/labels/p%3A%20image_picker) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20image_picker?label=)](https://github.com/flutter/plugins/labels/p%3A%20image_picker) | +| [in_app_purchase](./packages/in_app_purchase/) | [![pub package](https://img.shields.io/pub/v/in_app_purchase.svg)](https://pub.dev/packages/in_app_purchase) | [![pub points](https://img.shields.io/pub/points/in_app_purchase)](https://pub.dev/packages/in_app_purchase/score) | [![popularity](https://img.shields.io/pub/popularity/in_app_purchase)](https://pub.dev/packages/in_app_purchase/score) | [![likes](https://img.shields.io/pub/likes/in_app_purchase)](https://pub.dev/packages/in_app_purchase/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20in_app_purchase?label=)](https://github.com/flutter/flutter/labels/p%3A%20in_app_purchase) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20in_app_purchase?label=)](https://github.com/flutter/plugins/labels/p%3A%20in_app_purchase) | +| [ios_platform_images](./packages/ios_platform_images/) | [![pub package](https://img.shields.io/pub/v/ios_platform_images.svg)](https://pub.dev/packages/ios_platform_images) | [![pub points](https://img.shields.io/pub/points/ios_platform_images)](https://pub.dev/packages/ios_platform_images/score) | [![popularity](https://img.shields.io/pub/popularity/ios_platform_images)](https://pub.dev/packages/ios_platform_images/score) | [![likes](https://img.shields.io/pub/likes/ios_platform_images)](https://pub.dev/packages/ios_platform_images/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20ios_platform_images?label=)](https://github.com/flutter/flutter/labels/p%3A%20ios_platform_images) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20ios_platform_images?label=)](https://github.com/flutter/plugins/labels/p%3A%20ios_platform_images) | +| [local_auth](./packages/local_auth/) | [![pub package](https://img.shields.io/pub/v/local_auth.svg)](https://pub.dev/packages/local_auth) | [![pub points](https://img.shields.io/pub/points/local_auth)](https://pub.dev/packages/local_auth/score) | [![popularity](https://img.shields.io/pub/popularity/local_auth)](https://pub.dev/packages/local_auth/score) | [![likes](https://img.shields.io/pub/likes/local_auth)](https://pub.dev/packages/local_auth/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20local_auth?label=)](https://github.com/flutter/flutter/labels/p%3A%20local_auth) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20local_auth?label=)](https://github.com/flutter/plugins/labels/p%3A%20local_auth) | +| [path_provider](./packages/path_provider/) | [![pub package](https://img.shields.io/pub/v/path_provider.svg)](https://pub.dev/packages/path_provider) | [![pub points](https://img.shields.io/pub/points/path_provider)](https://pub.dev/packages/path_provider/score) | [![popularity](https://img.shields.io/pub/popularity/path_provider)](https://pub.dev/packages/path_provider/score) | [![likes](https://img.shields.io/pub/likes/path_provider)](https://pub.dev/packages/path_provider/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20path_provider?label=)](https://github.com/flutter/flutter/labels/p%3A%20path_provider) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20path_provider?label=)](https://github.com/flutter/plugins/labels/p%3A%20path_provider) | +| [plugin_platform_interface](./packages/plugin_platform_interface/) | [![pub package](https://img.shields.io/pub/v/plugin_platform_interface.svg)](https://pub.dev/packages/plugin_platform_interface) | [![pub points](https://img.shields.io/pub/points/plugin_platform_interface)](https://pub.dev/packages/plugin_platform_interface/score) | [![popularity](https://img.shields.io/pub/popularity/plugin_platform_interface)](https://pub.dev/packages/plugin_platform_interface/score) | [![likes](https://img.shields.io/pub/likes/plugin_platform_interface)](https://pub.dev/packages/plugin_platform_interface/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20plugin_platform_interface?label=)](https://github.com/flutter/flutter/labels/p%3A%20plugin_platform_interface) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20plugin_platform_interface?label=)](https://github.com/flutter/plugins/labels/p%3A%20plugin_platform_interface) | +| [quick_actions](./packages/quick_actions/) | [![pub package](https://img.shields.io/pub/v/quick_actions.svg)](https://pub.dev/packages/quick_actions) | [![pub points](https://img.shields.io/pub/points/quick_actions)](https://pub.dev/packages/quick_actions/score) | [![popularity](https://img.shields.io/pub/popularity/quick_actions)](https://pub.dev/packages/quick_actions/score) | [![likes](https://img.shields.io/pub/likes/quick_actions)](https://pub.dev/packages/quick_actions/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20quick_actions?label=)](https://github.com/flutter/flutter/labels/p%3A%20quick_actions) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20quick_actions?label=)](https://github.com/flutter/plugins/labels/p%3A%20quick_actions) | +| [shared_preferences](./packages/shared_preferences/) | [![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dev/packages/shared_preferences) | [![pub points](https://img.shields.io/pub/points/shared_preferences)](https://pub.dev/packages/shared_preferences/score) | [![popularity](https://img.shields.io/pub/popularity/shared_preferences)](https://pub.dev/packages/shared_preferences/score) | [![likes](https://img.shields.io/pub/likes/shared_preferences)](https://pub.dev/packages/shared_preferences/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20shared_preferences?label=)](https://github.com/flutter/flutter/labels/p%3A%20shared_preferences) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20shared_preferences?label=)](https://github.com/flutter/plugins/labels/p%3A%20shared_preferences) | +| [url_launcher](./packages/url_launcher/) | [![pub package](https://img.shields.io/pub/v/url_launcher.svg)](https://pub.dev/packages/url_launcher) | [![pub points](https://img.shields.io/pub/points/url_launcher)](https://pub.dev/packages/url_launcher/score) | [![popularity](https://img.shields.io/pub/popularity/url_launcher)](https://pub.dev/packages/url_launcher/score) | [![likes](https://img.shields.io/pub/likes/url_launcher)](https://pub.dev/packages/url_launcher/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20url_launcher?label=)](https://github.com/flutter/flutter/labels/p%3A%20url_launcher) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20url_launcher?label=)](https://github.com/flutter/plugins/labels/p%3A%20url_launcher) | +| [video_player](./packages/video_player/) | [![pub package](https://img.shields.io/pub/v/video_player.svg)](https://pub.dev/packages/video_player) | [![pub points](https://img.shields.io/pub/points/video_player)](https://pub.dev/packages/video_player/score) | [![popularity](https://img.shields.io/pub/popularity/video_player)](https://pub.dev/packages/video_player/score) | [![likes](https://img.shields.io/pub/likes/video_player)](https://pub.dev/packages/video_player/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20video_player?label=)](https://github.com/flutter/flutter/labels/p%3A%20video_player) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20video_player?label=)](https://github.com/flutter/plugins/labels/p%3A%20video_player) | +| [webview_flutter](./packages/webview_flutter/) | [![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dev/packages/webview_flutter) | [![pub points](https://img.shields.io/pub/points/webview_flutter)](https://pub.dev/packages/webview_flutter/score) | [![popularity](https://img.shields.io/pub/popularity/webview_flutter)](https://pub.dev/packages/webview_flutter/score) | [![likes](https://img.shields.io/pub/likes/webview_flutter)](https://pub.dev/packages/webview_flutter/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p:%20webview?label=)](https://github.com/flutter/flutter/labels/p%3A%20webview) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/plugins/p:%20webview_flutter?label=)](https://github.com/flutter/plugins/labels/p%3A%20webview_flutter) | diff --git a/analysis_options.yaml b/analysis_options.yaml index 60d2c33601eb..b12af6cf11e6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,41 +2,35 @@ # 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. -# -# Plugins that have not yet switched from the previous set of options have a -# local analysis_options.yaml that points to analysis_options_legacy.yaml -# instead. # Specify analysis options. # -# Until there are meta linter rules, each desired lint must be explicitly enabled. -# See: https://github.com/dart-lang/linter/issues/288 -# # 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/master/pkg/analyzer#configuring-the-analyzer +# 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/plugins/blob/master/analysis_options.yaml -# - https://github.com/flutter/engine/blob/master/analysis_options.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 by Flutter tools, such as IntelliJ, -# Android Studio, and the `flutter analyze` command. +# This file contains the analysis options used for code in the flutter/plugins +# repository. analyzer: - strong-mode: - implicit-casts: false - implicit-dynamic: false + 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 TODOs in the code + # 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) @@ -45,13 +39,11 @@ analyzer: # 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. - unnecessary_null_comparison: false always_require_non_null_named_parameters: false # not needed with nnbd - # TODO(https://github.com/flutter/flutter/issues/74381): - # Clean up existing unnecessary imports, and remove line to ignore. - unnecessary_import: ignore exclude: # Ignore generated files - '**/*.g.dart' @@ -61,8 +53,7 @@ analyzer: linter: rules: - # these rules are documented on and in the same order as - # the Dart Lint rules page to make maintenance easier + # This list is derived from the list of all available lints located at # https://github.com/dart-lang/linter/blob/master/example/all.yaml - always_declare_return_types - always_put_control_body_on_new_line @@ -72,62 +63,68 @@ linter: # - always_use_package_imports # we do this commonly - annotate_overrides # - avoid_annotating_with_dynamic # conflicts with always_specify_types - # - avoid_as # required for implicit-casts: true - avoid_bool_literals_in_conditional_expressions - # - avoid_catches_without_on_clauses # we do this commonly - # - avoid_catching_errors # we do this commonly + # - avoid_catches_without_on_clauses # blocked on https://github.com/dart-lang/linter/issues/3023 + # - avoid_catching_errors # blocked on https://github.com/dart-lang/linter/issues/3023 - avoid_classes_with_only_static_members - # - avoid_double_and_int_checks # only useful when targeting JS runtime + - avoid_double_and_int_checks + # - avoid_dynamic_calls # LOCAL CHANGE - Needs to be enabled and violations fixed. - avoid_empty_else - avoid_equals_and_hash_code_on_mutable_classes - # - avoid_escaping_inner_quotes # not yet tested + - avoid_escaping_inner_quotes - avoid_field_initializers_in_const_classes + # - avoid_final_parameters # incompatible with prefer_final_parameters - avoid_function_literals_in_foreach_calls - # - avoid_implementing_value_types # not yet tested + # - avoid_implementing_value_types # LOCAL CHANGE - Needs to be enabled and violations fixed. - avoid_init_to_null - # - avoid_js_rounded_ints # only useful when targeting JS runtime + - avoid_js_rounded_ints + # - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to - avoid_null_checks_in_equality_operators - # - avoid_positional_boolean_parameters # not yet tested - # - avoid_print # not yet tested + # - avoid_positional_boolean_parameters # would have been nice to enable this but by now there's too many places that break it + # - avoid_print # LOCAL CHANGE - Needs to be enabled and violations fixed. # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) - # - avoid_redundant_argument_values # not yet tested + - avoid_redundant_argument_values - avoid_relative_lib_imports - avoid_renaming_method_parameters - avoid_return_types_on_setters - # - avoid_returning_null # there are plenty of valid reasons to return null - # - avoid_returning_null_for_future # not yet tested + # - avoid_returning_null # still violated by some pre-nnbd code that we haven't yet migrated + - avoid_returning_null_for_future - avoid_returning_null_for_void - # - avoid_returning_this # there are plenty of valid reasons to return this - # - avoid_setters_without_getters # not yet tested + # - avoid_returning_this # there are enough valid reasons to return `this` that this lint ends up with too many false positives + - avoid_setters_without_getters - avoid_shadowing_type_parameters - avoid_single_cascade_in_expression_statements - avoid_slow_async_io - # - avoid_type_to_string # we do this commonly + - avoid_type_to_string - avoid_types_as_parameter_names # - avoid_types_on_closure_parameters # conflicts with always_specify_types - # - avoid_unnecessary_containers # not yet tested + - avoid_unnecessary_containers - avoid_unused_constructor_parameters - avoid_void_async - # - avoid_web_libraries_in_flutter # not yet tested + # - avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere - await_only_futures - camel_case_extensions - camel_case_types - cancel_subscriptions - # - cascade_invocations # not yet tested + # - cascade_invocations # doesn't match the typical style of this repo - cast_nullable_to_non_nullable # - close_sinks # not reliable enough - # - comment_references # blocked on https://github.com/flutter/flutter/issues/20765 + # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 + # - conditional_uri_does_not_exist # not yet tested # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 - control_flow_in_finally # - curly_braces_in_flow_control_structures # not required by flutter style - # - diagnostic_describe_all_properties # not yet tested + - depend_on_referenced_packages + - deprecated_consistency + # - diagnostic_describe_all_properties # enabled only at the framework level (packages/flutter/lib) - directives_ordering - # - do_not_use_environment # we do this commonly + # - do_not_use_environment # there are appropriate times to use the environment, especially in our tests and build logic - empty_catches - empty_constructor_bodies - empty_statements + - eol_at_end_of_file - exhaustive_cases - # - file_names # not yet tested + - file_names - flutter_style_todos - hash_and_equals - implementation_imports @@ -137,24 +134,28 @@ linter: - leading_newlines_in_multiline_strings - library_names - library_prefixes + - library_private_types_in_public_api # - lines_longer_than_80_chars # not required by flutter style - list_remove_unrelated_type - # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 - # - missing_whitespace_between_adjacent_strings # not yet tested + # - 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 # too many false positives + # - no_default_cases # LOCAL CHANGE - Needs to be enabled and violations fixed. - 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/ - non_constant_identifier_names + - noop_primitive_operations - null_check_on_nullable_type_parameter - # - null_closures # not required by flutter style + - null_closures # - omit_local_variable_types # opposite of always_specify_types # - one_member_abstracts # too many false positives - # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 + # - 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. - overridden_fields - package_api_docs - # - package_names # non conforming packages in sdk + - package_names - package_prefixed_library_names # - parameter_assignments # we do this commonly - prefer_adjacent_string_concatenation @@ -174,74 +175,90 @@ linter: - prefer_final_fields - prefer_final_in_for_each - prefer_final_locals + # - prefer_final_parameters # we should enable this one day when it can be auto-fixed (https://github.com/dart-lang/linter/issues/3104), see also parameter_assignments - prefer_for_elements_to_map_fromIterable - prefer_foreach - # - prefer_function_declarations_over_variables # not yet tested + - prefer_function_declarations_over_variables - prefer_generic_function_type_aliases - prefer_if_elements_to_conditional_expressions - prefer_if_null_operators - prefer_initializing_formals - prefer_inlined_adds - # - prefer_int_literals # not yet tested - # - prefer_interpolation_to_compose_strings # not yet tested + # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants + - prefer_interpolation_to_compose_strings - prefer_is_empty - prefer_is_not_empty - prefer_is_not_operator - prefer_iterable_whereType - # - prefer_mixin # https://github.com/dart-lang/language/issues/32 - # - prefer_null_aware_operators # disable until NNBD, see https://github.com/flutter/flutter/pull/32711#issuecomment-492930932 - # - prefer_relative_imports # not yet tested + # - prefer_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018 + # - prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere + - prefer_null_aware_operators + - prefer_relative_imports - prefer_single_quotes - prefer_spread_collections - prefer_typing_uninitialized_variables - prefer_void_to_null - # - provide_deprecation_message # not yet tested + - provide_deprecation_message # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml - recursive_getters - # - sized_box_for_whitespace # not yet tested + # - require_trailing_commas # blocked on https://github.com/dart-lang/sdk/issues/47441 + - secure_pubspec_urls + # - sized_box_for_whitespace # LOCAL CHANGE - Needs to be enabled and violations fixed. + # - sized_box_shrink_expand # not yet tested - slash_for_doc_comments - # - sort_child_properties_last # not yet tested + - sort_child_properties_last - sort_constructors_first + # - sort_pub_dependencies # prevents separating pinned transitive dependencies - sort_unnamed_constructors_first - test_types_in_equals - throw_in_finally - tighten_type_of_initializing_formals # - type_annotate_public_apis # subset of always_specify_types - type_init_formals - # - unawaited_futures # too many false positives - # - unnecessary_await_in_return # not yet tested + # - unawaited_futures # too many false positives, especially with the way AnimationController works + # - unnecessary_await_in_return # LOCAL CHANGE - Needs to be enabled and violations fixed. - unnecessary_brace_in_string_interps - unnecessary_const + - unnecessary_constructor_name # - unnecessary_final # conflicts with prefer_final_locals - unnecessary_getters_setters # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 + - unnecessary_late - unnecessary_new - unnecessary_null_aware_assignments - # - unnecessary_null_checks # not yet tested + - unnecessary_null_checks - unnecessary_null_in_if_null_operators - unnecessary_nullable_for_final_variable_declarations - unnecessary_overrides - unnecessary_parenthesis - # - unnecessary_raw_strings # not yet tested + # - unnecessary_raw_strings # what's "necessary" is a matter of opinion; consistency across strings can help readability more than this lint - unnecessary_statements - unnecessary_string_escapes - unnecessary_string_interpolations - unnecessary_this - unrelated_type_equality_checks - # - unsafe_html # not yet tested + - unsafe_html + # - use_build_context_synchronously # LOCAL CHANGE - Needs to be enabled and violations fixed. + # - use_colored_box # not yet tested + # - use_decorated_box # not yet tested + # - use_enums # not yet tested - use_full_hex_values_for_flutter_colors - # - use_function_type_syntax_for_parameters # not yet tested + - use_function_type_syntax_for_parameters + - use_if_null_to_convert_nulls_to_bools - use_is_even_rather_than_modulo - # - use_key_in_widget_constructors # not yet tested + - use_key_in_widget_constructors - use_late_for_private_fields_and_variables + - use_named_constants - use_raw_strings - use_rethrow_when_possible - # - use_setters_to_change_properties # not yet tested + - use_setters_to_change_properties # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 + - use_super_parameters + - use_test_throws_matchers # - 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 changes ### + ### 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 diff --git a/analysis_options_legacy.yaml b/analysis_options_legacy.yaml deleted file mode 100644 index b2a343f220c8..000000000000 --- a/analysis_options_legacy.yaml +++ /dev/null @@ -1,17 +0,0 @@ -include: package:pedantic/analysis_options.1.8.0.yaml -analyzer: - exclude: - # Ignore generated files - - '**/*.g.dart' - - 'lib/src/generated/*.dart' - - '**/*.mocks.dart' # Mockito @GenerateMocks - - '**/*.pigeon.dart' # Pigeon generated file - errors: - always_require_non_null_named_parameters: false # not needed with nnbd - # TODO(https://github.com/flutter/flutter/issues/74381): - # Clean up existing unnecessary imports, and remove line to ignore. - unnecessary_import: ignore - unnecessary_null_comparison: false # Turned as long as nnbd mix-mode is supported. -linter: - rules: - - public_member_api_docs diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 9ef626a8ad2b..127da4561bc3 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,101 @@ +## 0.10.0+4 + +* Removes usage of `_ambiguate` method in example. +* Updates minimum Flutter version to 3.0. + +## 0.10.0+3 + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. + +## 0.10.0+2 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. + +## 0.10.0+1 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 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 + `CameraAccessDenied` and `AudioAccessDenied`. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). + +## 0.9.8+1 + +* Ignores deprecation warnings for upcoming styleFrom button API changes. + +## 0.9.8 + +* Moves Android and iOS implementations to federated packages. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/104231). + +## 0.9.7+1 + +* Moves streaming implementation to the platform interface package. + +## 0.9.7 + +* Returns all the available cameras on iOS. + +## 0.9.6 + +* Adds audio access permission handling logic on iOS to fix an issue with `prepareForVideoRecording` not awaiting for the audio permission request result. + +## 0.9.5+1 + +* Suppresses warnings for pre-iOS-11 codepaths. + +## 0.9.5 + +* Adds camera access permission handling logic on iOS to fix a related crash when using the camera for the first time. + +## 0.9.4+24 + +* Fixes preview orientation when pausing preview with locked orientation. + +## 0.9.4+23 + +* Minor fixes for new analysis options. + +## 0.9.4+22 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.9.4+21 + +* Fixes README code samples. + +## 0.9.4+20 + +* Fixes an issue with the orientation of videos recorded in landscape on Android. + +## 0.9.4+19 + +* Migrate deprecated Scaffold SnackBar methods to ScaffoldMessenger. + +## 0.9.4+18 + +* Fixes a crash in iOS when streaming on low-performance devices. + +## 0.9.4+17 + +* Removes obsolete information from README, and adds OS support table. + +## 0.9.4+16 + +* Fixes a bug resulting in a `CameraAccessException` that prevents image + capture on some Android devices. + +## 0.9.4+15 + +* Uses dispatch queue for pixel buffer synchronization on iOS. +* Minor iOS internal code cleanup related to queue helper functions. + ## 0.9.4+14 * Restores compatibility with Flutter 2.5 and 2.8. diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md index 6cca254f8690..4d7c3d90791a 100644 --- a/packages/camera/camera/README.md +++ b/packages/camera/camera/README.md @@ -1,10 +1,14 @@ # Camera Plugin + + [![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dev/packages/camera) A Flutter plugin for iOS, Android and Web allowing access to the device cameras. -*Note*: This plugin is still under development, and some APIs might not be available yet. We are working on a refactor which can be followed here: [issue](https://github.com/flutter/flutter/issues/31225) +| | Android | iOS | Web | +|----------------|---------|----------|------------------------| +| **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | ## Features @@ -19,8 +23,9 @@ First, add `camera` as a [dependency in your pubspec.yaml file](https://flutter. ### iOS -The camera plugin functionality works on iOS 10.0 or higher. If compiling for any version lower than 10.0, -make sure to programmatically check the version of iOS running on the device before using any camera plugin features. +\* The camera plugin compiles for any version of iOS, but its functionality +requires iOS 10 or higher. If compiling for iOS 9, make sure to programmatically +check the version of iOS running on the device before using any camera plugin features. The [device_info_plus](https://pub.dev/packages/device_info_plus) plugin, for example, can be used to check the iOS version. Add two rows to the `ios/Runner/Info.plist`: @@ -28,20 +33,20 @@ Add two rows to the `ios/Runner/Info.plist`: * one with the key `Privacy - Camera Usage Description` and a usage description. * and one with the key `Privacy - Microphone Usage Description` and a usage description. -Or in text format add the key: +If editing `Info.plist` as text, add: ```xml NSCameraUsageDescription -Can I use the camera please? +your usage description here NSMicrophoneUsageDescription -Can I use the mic please? +your usage description here ``` ### Android Change the minimum Android sdk version to 21 (or higher) in your `android/app/build.gradle` file. -``` +```groovy minSdkVersion 21 ``` @@ -54,66 +59,101 @@ For web integration details, see the ### Handling Lifecycle states -As of version [0.5.0](https://github.com/flutter/plugins/blob/master/packages/camera/CHANGELOG.md#050) of the camera plugin, lifecycle changes are no longer handled by the plugin. This means developers are now responsible to control camera resources when the lifecycle state is updated. Failure to do so might lead to unexpected behavior (for example as described in issue [#39109](https://github.com/flutter/flutter/issues/39109)). Handling lifecycle changes can be done by overriding the `didChangeAppLifecycleState` method like so: +As of version [0.5.0](https://github.com/flutter/plugins/blob/main/packages/camera/CHANGELOG.md#050) of the camera plugin, lifecycle changes are no longer handled by the plugin. This means developers are now responsible to control camera resources when the lifecycle state is updated. Failure to do so might lead to unexpected behavior (for example as described in issue [#39109](https://github.com/flutter/flutter/issues/39109)). Handling lifecycle changes can be done by overriding the `didChangeAppLifecycleState` method like so: + ```dart - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - // App state changed before we got the chance to initialize. - if (controller == null || !controller.value.isInitialized) { - return; - } - if (state == AppLifecycleState.inactive) { - controller?.dispose(); - } else if (state == AppLifecycleState.resumed) { - if (controller != null) { - onNewCameraSelected(controller.description); - } - } +@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); } +} ``` +### Handling camera access permissions + +Permission errors may be thrown when initializing the camera controller, and you are expected to handle them properly. + +Here is a list of all permission error codes that can be thrown: + +- `CameraAccessDenied`: Thrown when user denies the camera access permission. + +- `CameraAccessDeniedWithoutPrompt`: iOS only for now. Thrown when user has previously denied the permission. iOS does not allow prompting alert dialog a second time. Users will have to go to Settings > Privacy > Camera in order to enable camera access. + +- `CameraAccessRestricted`: iOS only for now. Thrown when camera access is restricted and users cannot grant permission (parental control). + +- `AudioAccessDenied`: Thrown when user denies the audio access permission. + +- `AudioAccessDeniedWithoutPrompt`: iOS only for now. Thrown when user has previously denied the permission. iOS does not allow prompting alert dialog a second time. Users will have to go to Settings > Privacy > Microphone in order to enable audio access. + +- `AudioAccessRestricted`: iOS only for now. Thrown when audio access is restricted and users cannot grant permission (parental control). + ### Example Here is a small example flutter app displaying a full screen camera preview. + ```dart -import 'dart:async'; -import 'package:flutter/material.dart'; import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; -List cameras; +late List _cameras; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - cameras = await availableCameras(); - runApp(CameraApp()); + _cameras = await availableCameras(); + runApp(const CameraApp()); } +/// CameraApp is the Main Application. class CameraApp extends StatefulWidget { + /// Default Constructor + const CameraApp({Key? key}) : super(key: key); + @override - _CameraAppState createState() => _CameraAppState(); + State createState() => _CameraAppState(); } class _CameraAppState extends State { - CameraController controller; + late CameraController controller; @override void initState() { super.initState(); - controller = CameraController(cameras[0], ResolutionPreset.max); + controller = CameraController(_cameras[0], ResolutionPreset.max); controller.initialize().then((_) { if (!mounted) { return; } setState(() {}); + }).catchError((Object e) { + if (e is CameraException) { + switch (e.code) { + case 'CameraAccessDenied': + print('User denied camera access.'); + break; + default: + print('Handle other errors.'); + break; + } + } }); } @override void dispose() { - controller?.dispose(); + controller.dispose(); super.dispose(); } @@ -127,11 +167,8 @@ class _CameraAppState extends State { ); } } - ``` For a more elaborate usage example see [here](https://github.com/flutter/plugins/tree/main/packages/camera/camera/example). -*Note*: This plugin is still under development, and some APIs might not be available yet. -[Feedback welcome](https://github.com/flutter/flutter/issues) and -[Pull Requests](https://github.com/flutter/plugins/pulls) are most welcome! +[1]: https://pub.dev/packages/camera_web#limitations-on-the-web-platform diff --git a/packages/camera/camera/android/settings.gradle b/packages/camera/camera/android/settings.gradle deleted file mode 100644 index 5222c9172f70..000000000000 --- a/packages/camera/camera/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'camera' diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java deleted file mode 100644 index ecb96a88f31a..000000000000 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java +++ /dev/null @@ -1,27 +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. - -package io.flutter.plugins.camera; - -import static junit.framework.TestCase.assertEquals; - -import android.content.pm.PackageManager; -import io.flutter.plugins.camera.CameraPermissions.CameraRequestPermissionsListener; -import org.junit.Test; - -public class CameraPermissionsTest { - @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]); - } -} diff --git a/packages/camera/camera/example/android/app/build.gradle b/packages/camera/camera/example/android/app/build.gradle index 476d65373723..5d6af5887012 100644 --- a/packages/camera/camera/example/android/app/build.gradle +++ b/packages/camera/camera/example/android/app/build.gradle @@ -57,7 +57,7 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/packages/camera/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/camera/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/camera/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/camera/camera/example/android/build.gradle b/packages/camera/camera/example/android/build.gradle index 456d020f6e2c..c21bff8e0a2f 100644 --- a/packages/camera/camera/example/android/build.gradle +++ b/packages/camera/camera/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.0.1' } } diff --git a/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties index 01a286e96a21..297f2fec363f 100644 --- a/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/camera/camera/example/build.excerpt.yaml b/packages/camera/camera/example/build.excerpt.yaml new file mode 100644 index 000000000000..e317efa11cb3 --- /dev/null +++ b/packages/camera/camera/example/build.excerpt.yaml @@ -0,0 +1,15 @@ +targets: + $default: + sources: + include: + - lib/** + # Some default includes that aren't really used here but will prevent + # false-negative warnings: + - $package$ + - lib/$lib$ + exclude: + - '**/.*/**' + - '**/build/**' + builders: + code_excerpter|code_excerpter: + enabled: true diff --git a/packages/camera/camera/example/integration_test/camera_test.dart b/packages/camera/camera/example/integration_test/camera_test.dart index 557f4858acab..b233d4958c8a 100644 --- a/packages/camera/camera/example/integration_test/camera_test.dart +++ b/packages/camera/camera/example/integration_test/camera_test.dart @@ -221,16 +221,16 @@ void main() { ); await controller.initialize(); - bool _isDetecting = false; + bool isDetecting = false; await controller.startImageStream((CameraImage image) { - if (_isDetecting) { + if (isDetecting) { return; } - _isDetecting = true; + isDetecting = true; - expectLater(image, isNotNull).whenComplete(() => _isDetecting = false); + expectLater(image, isNotNull).whenComplete(() => isDetecting = false); }); expect(controller.value.isStreamingImages, true); @@ -254,19 +254,19 @@ void main() { ); await controller.initialize(); - final Completer _completer = Completer(); + final Completer completer = Completer(); await controller.startImageStream((CameraImage image) { - if (!_completer.isCompleted) { + if (!completer.isCompleted) { Future(() async { await controller.stopImageStream(); await controller.dispose(); }).then((Object? value) { - _completer.complete(image); + completer.complete(image); }); } }); - return _completer.future; + return completer.future; } testWidgets( @@ -277,20 +277,20 @@ void main() { return; } - CameraImage _image = await startStreaming(cameras, null); - expect(_image, isNotNull); - expect(_image.format.group, ImageFormatGroup.bgra8888); - expect(_image.planes.length, 1); + CameraImage image = await startStreaming(cameras, null); + expect(image, isNotNull); + expect(image.format.group, ImageFormatGroup.bgra8888); + expect(image.planes.length, 1); - _image = await startStreaming(cameras, ImageFormatGroup.yuv420); - expect(_image, isNotNull); - expect(_image.format.group, ImageFormatGroup.yuv420); - expect(_image.planes.length, 2); + image = await startStreaming(cameras, ImageFormatGroup.yuv420); + expect(image, isNotNull); + expect(image.format.group, ImageFormatGroup.yuv420); + expect(image.planes.length, 2); - _image = await startStreaming(cameras, ImageFormatGroup.bgra8888); - expect(_image, isNotNull); - expect(_image.format.group, ImageFormatGroup.bgra8888); - expect(_image.planes.length, 1); + image = await startStreaming(cameras, ImageFormatGroup.bgra8888); + expect(image, isNotNull); + expect(image.format.group, ImageFormatGroup.bgra8888); + expect(image.planes.length, 1); }, skip: !Platform.isIOS, ); diff --git a/packages/camera/camera/example/ios/Podfile b/packages/camera/camera/example/ios/Podfile index 5bc7b7e85717..f7d6a5e68c3a 100644 --- a/packages/camera/camera/example/ios/Podfile +++ b/packages/camera/camera/example/ios/Podfile @@ -29,13 +29,6 @@ flutter_ios_podfile_setup target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - - target 'RunnerTests' do - platform :ios, '9.0' - inherit! :search_paths - # Pods for testing - pod 'OCMock', '~> 3.8.1' - end end post_install do |installer| diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 5f788fc9b9f9..99433b084f27 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,42 +7,16 @@ objects = { /* Begin PBXBuildFile section */ - 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */; }; - 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB766A2665316900CE5A93 /* CameraFocusTests.m */; }; - 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 236906D1621AE863A5B2E770 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 89D82918721FABF772705DB0 /* libPods-Runner.a */; }; - 25C3919135C3D981E6F800D0 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */; }; - 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - E01EE4A82799F3A5008C1950 /* QueueHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */; }; - E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */; }; - E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */; }; - E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */; }; - E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */; }; - E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */; }; - E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */; }; - E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */; }; - E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */; }; - E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; }; - F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */ = {isa = PBXBuildFile; fileRef = F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 03BB766D2665316900CE5A93 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -57,12 +31,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraMethodChannelTests.m; sourceTree = ""; }; - 03BB76682665316900CE5A93 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 03BB766A2665316900CE5A93 /* CameraFocusTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraFocusTests.m; sourceTree = ""; }; - 03BB766C2665316900CE5A93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraOrientationTests.m; sourceTree = ""; }; - 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeFlutterResultTests.m; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 14AE82C910C2A12F2ECB2094 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; @@ -83,29 +51,9 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9C5CC6CAD53AD388B2694F3A /* 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 = ""; }; A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QueueHelperTests.m; sourceTree = ""; }; - E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraCaptureSessionQueueRaceConditionTests.m; sourceTree = ""; }; - E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTSavePhotoDelegateTests.m; sourceTree = ""; }; - E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTCamPhotoCaptureTests.m; sourceTree = ""; }; - E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTCamSampleBufferTests.m; sourceTree = ""; }; - E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = ""; }; - E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = ""; }; - E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = ""; }; - E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPropertiesTests.m; sourceTree = ""; }; - E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = ""; }; - F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFLTThreadSafeFlutterResult.h; sourceTree = ""; }; - F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFLTThreadSafeFlutterResult.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 03BB76652665316900CE5A93 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 25C3919135C3D981E6F800D0 /* libPods-RunnerTests.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -117,30 +65,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 03BB76692665316900CE5A93 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 03BB766A2665316900CE5A93 /* CameraFocusTests.m */, - 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */, - 03BB766C2665316900CE5A93 /* Info.plist */, - 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */, - 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */, - E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */, - E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */, - E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */, - E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */, - E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */, - E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */, - E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */, - E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, - F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */, - F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */, - E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */, - E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */, - ); - path = RunnerTests; - sourceTree = ""; - }; 3242FD2B467C15C62200632F /* Frameworks */ = { isa = PBXGroup; children = ( @@ -166,7 +90,6 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - 03BB76692665316900CE5A93 /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, FD386F00E98D73419C929072 /* Pods */, 3242FD2B467C15C62200632F /* Frameworks */, @@ -177,7 +100,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - 03BB76682665316900CE5A93 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -220,25 +142,6 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 03BB76672665316900CE5A93 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 03BB76712665316900CE5A93 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 422786A96136AA9087A2041B /* [CP] Check Pods Manifest.lock */, - 03BB76642665316900CE5A93 /* Sources */, - 03BB76652665316900CE5A93 /* Frameworks */, - 03BB76662665316900CE5A93 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 03BB766E2665316900CE5A93 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = camera_exampleTests; - productReference = 03BB76682665316900CE5A93 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -269,11 +172,6 @@ LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { - 03BB76672665316900CE5A93 = { - CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; }; @@ -293,19 +191,11 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - 03BB76672665316900CE5A93 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 03BB76662665316900CE5A93 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -334,28 +224,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 422786A96136AA9087A2041B /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -395,28 +263,6 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 03BB76642665316900CE5A93 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */, - 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */, - E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */, - E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */, - 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, - E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, - E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */, - E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */, - F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */, - 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, - E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */, - E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */, - E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */, - E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */, - E01EE4A82799F3A5008C1950 /* QueueHelperTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -429,14 +275,6 @@ }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 03BB766E2665316900CE5A93 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 03BB766D2665316900CE5A93 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -457,57 +295,6 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 03BB766F2665316900CE5A93 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "dev.flutter.plugins.cameraExample.camera-exampleTests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Debug; - }; - 03BB76702665316900CE5A93 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "dev.flutter.plugins.cameraExample.camera-exampleTests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Release; - }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -661,15 +448,6 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 03BB76712665316900CE5A93 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 03BB766F2665316900CE5A93 /* Debug */, - 03BB76702665316900CE5A93 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/packages/camera/camera/example/ios/Runner/main.m b/packages/camera/camera/example/ios/Runner/main.m index 8b3fb8d5b361..d1224fea37ed 100644 --- a/packages/camera/camera/example/ios/Runner/main.m +++ b/packages/camera/camera/example/ios/Runner/main.m @@ -11,7 +11,7 @@ int main(int argc, char *argv[]) { // The setup logic in `AppDelegate::didFinishLaunchingWithOptions:` eventually sends camera // operations on the background queue, which would run concurrently with the test cases during // unit tests, making the debugging process confusing. This setup is actually not necessary for - // the unit tests, so here we want to skip the AppDelegate when running unit tests. + // the unit tests, so it is better to skip the AppDelegate when running unit tests. BOOL isTesting = NSClassFromString(@"XCTestCase") != nil; return UIApplicationMain(argc, argv, nil, isTesting ? nil : NSStringFromClass([AppDelegate class])); diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m deleted file mode 100644 index ccc8de5b23bd..000000000000 --- a/packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m +++ /dev/null @@ -1,37 +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 camera; -@import camera.Test; -@import AVFoundation; -@import XCTest; -#import - -@interface FLTCamSampleBufferTests : XCTestCase - -@end - -@implementation FLTCamSampleBufferTests - -- (void)testSampleBufferCallbackQueueMustBeCaptureSessionQueue { - id inputMock = OCMClassMock([AVCaptureDeviceInput class]); - OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) - .andReturn(inputMock); - - id sessionMock = OCMClassMock([AVCaptureSession class]); - OCMStub([sessionMock alloc]).andReturn(sessionMock); - OCMStub([sessionMock addInputWithNoConnections:[OCMArg any]]); // no-op - OCMStub([sessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); - - dispatch_queue_t captureSessionQueue = dispatch_queue_create("testing", NULL); - FLTCam *cam = [[FLTCam alloc] initWithCameraName:@"camera" - resolutionPreset:@"medium" - enableAudio:true - orientation:UIDeviceOrientationPortrait - captureSessionQueue:captureSessionQueue - error:nil]; - XCTAssertEqual(captureSessionQueue, cam.captureVideoOutput.sampleBufferCallbackQueue); -} - -@end diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index d47edfed69e2..860263edf2d3 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -2,19 +2,22 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: public_member_api_docs - import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:video_player/video_player.dart'; +/// Camera example home widget. class CameraExampleHome extends StatefulWidget { + /// Default Constructor + const CameraExampleHome({Key? key}) : super(key: key); + @override - _CameraExampleHomeState createState() { + State createState() { return _CameraExampleHomeState(); } } @@ -33,7 +36,7 @@ IconData getCameraLensIcon(CameraLensDirection direction) { } } -void logError(String code, String? message) { +void _logError(String code, String? message) { if (message != null) { print('Error: $code\nError Message: $message'); } else { @@ -69,7 +72,7 @@ class _CameraExampleHomeState extends State @override void initState() { super.initState(); - _ambiguate(WidgetsBinding.instance)?.addObserver(this); + WidgetsBinding.instance.addObserver(this); _flashModeControlRowAnimationController = AnimationController( duration: const Duration(milliseconds: 300), @@ -99,12 +102,13 @@ class _CameraExampleHomeState extends State @override void dispose() { - _ambiguate(WidgetsBinding.instance)?.removeObserver(this); + WidgetsBinding.instance.removeObserver(this); _flashModeControlRowAnimationController.dispose(); _exposureModeControlRowAnimationController.dispose(); super.dispose(); } + // #docregion AppLifecycle @override void didChangeAppLifecycleState(AppLifecycleState state) { final CameraController? cameraController = controller; @@ -120,13 +124,11 @@ class _CameraExampleHomeState extends State onNewCameraSelected(cameraController.description); } } - - final GlobalKey _scaffoldKey = GlobalKey(); + // #enddocregion AppLifecycle @override Widget build(BuildContext context) { return Scaffold( - key: _scaffoldKey, appBar: AppBar( title: const Text('Camera example'), ), @@ -134,12 +136,6 @@ class _CameraExampleHomeState extends State children: [ Expanded( child: Container( - child: Padding( - padding: const EdgeInsets.all(1.0), - child: Center( - child: _cameraPreviewWidget(), - ), - ), decoration: BoxDecoration( color: Colors.black, border: Border.all( @@ -150,6 +146,12 @@ class _CameraExampleHomeState extends State width: 3.0, ), ), + child: Padding( + padding: const EdgeInsets.all(1.0), + child: Center( + child: _cameraPreviewWidget(), + ), + ), ), ), _captureControlRowWidget(), @@ -157,7 +159,6 @@ class _CameraExampleHomeState extends State Padding( padding: const EdgeInsets.all(5.0), child: Row( - mainAxisAlignment: MainAxisAlignment.start, children: [ _cameraTogglesRowWidget(), _thumbnailWidget(), @@ -233,6 +234,8 @@ class _CameraExampleHomeState extends State Container() else SizedBox( + width: 64.0, + height: 64.0, child: (localVideoController == null) ? ( // The captured image on the web contains a network-accessible URL @@ -243,6 +246,8 @@ class _CameraExampleHomeState extends State ? Image.network(imageFile!.path) : Image.file(File(imageFile!.path))) : Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.pink)), child: Center( child: AspectRatio( aspectRatio: @@ -251,11 +256,7 @@ class _CameraExampleHomeState extends State : 1.0, child: VideoPlayer(localVideoController)), ), - decoration: BoxDecoration( - border: Border.all(color: Colors.pink)), ), - width: 64.0, - height: 64.0, ), ], ), @@ -269,7 +270,6 @@ class _CameraExampleHomeState extends State children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.max, children: [ IconButton( icon: const Icon(Icons.flash_on), @@ -323,7 +323,6 @@ class _CameraExampleHomeState extends State child: ClipRect( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.max, children: [ IconButton( icon: const Icon(Icons.flash_off), @@ -369,11 +368,15 @@ class _CameraExampleHomeState extends State 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, @@ -391,10 +394,8 @@ class _CameraExampleHomeState extends State ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.max, children: [ TextButton( - child: const Text('AUTO'), style: styleAuto, onPressed: controller != null ? () => @@ -406,21 +407,22 @@ class _CameraExampleHomeState extends State showInSnackBar('Resetting exposure point'); } }, + child: const Text('AUTO'), ), TextButton( - child: const Text('LOCKED'), style: styleLocked, onPressed: controller != null ? () => onSetExposureModeButtonPressed(ExposureMode.locked) : null, + child: const Text('LOCKED'), ), TextButton( - child: const Text('RESET OFFSET'), style: styleLocked, onPressed: controller != null ? () => controller!.setExposureOffset(0.0) : null, + child: const Text('RESET OFFSET'), ), ], ), @@ -429,7 +431,6 @@ class _CameraExampleHomeState extends State ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.max, children: [ Text(_minAvailableExposureOffset.toString()), Slider( @@ -454,11 +455,15 @@ class _CameraExampleHomeState extends State 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, @@ -476,10 +481,8 @@ class _CameraExampleHomeState extends State ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.max, children: [ TextButton( - child: const Text('AUTO'), style: styleAuto, onPressed: controller != null ? () => onSetFocusModeButtonPressed(FocusMode.auto) @@ -490,13 +493,14 @@ class _CameraExampleHomeState extends State } showInSnackBar('Resetting focus point'); }, + child: const Text('AUTO'), ), TextButton( - child: const Text('LOCKED'), style: styleLocked, onPressed: controller != null ? () => onSetFocusModeButtonPressed(FocusMode.locked) : null, + child: const Text('LOCKED'), ), ], ), @@ -513,7 +517,6 @@ class _CameraExampleHomeState extends State return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.max, children: [ IconButton( icon: const Icon(Icons.camera_alt), @@ -573,19 +576,21 @@ class _CameraExampleHomeState extends State Widget _cameraTogglesRowWidget() { final List toggles = []; - final Null Function(CameraDescription? description) onChanged = - (CameraDescription? description) { + void onChanged(CameraDescription? description) { if (description == null) { return; } onNewCameraSelected(description); - }; + } - if (cameras.isEmpty) { - return const Text('No camera found'); + if (_cameras.isEmpty) { + SchedulerBinding.instance.addPostFrameCallback((_) async { + showInSnackBar('No camera found.'); + }); + return const Text('None'); } else { - for (final CameraDescription cameraDescription in cameras) { + for (final CameraDescription cameraDescription in _cameras) { toggles.add( SizedBox( width: 90.0, @@ -609,8 +614,8 @@ class _CameraExampleHomeState extends State String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); void showInSnackBar(String message) { - // ignore: deprecated_member_use - _scaffoldKey.currentState?.showSnackBar(SnackBar(content: Text(message))); + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(message))); } void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { @@ -629,8 +634,15 @@ class _CameraExampleHomeState extends State } Future onNewCameraSelected(CameraDescription cameraDescription) async { - if (controller != null) { - await controller!.dispose(); + 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( @@ -674,7 +686,33 @@ class _CameraExampleHomeState extends State .then((double value) => _minAvailableZoom = value), ]); } on CameraException catch (e) { - _showCameraException(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) { @@ -1011,36 +1049,33 @@ class _CameraExampleHomeState extends State } void _showCameraException(CameraException e) { - logError(e.code, e.description); + _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({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { - return MaterialApp( + return const MaterialApp( home: CameraExampleHome(), ); } } -List cameras = []; +List _cameras = []; Future main() async { // Fetch the available cameras before initializing the app. try { WidgetsFlutterBinding.ensureInitialized(); - cameras = await availableCameras(); + _cameras = await availableCameras(); } on CameraException catch (e) { - logError(e.code, e.description); + _logError(e.code, e.description); } - runApp(CameraApp()); + runApp(const CameraApp()); } - -/// 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. -// TODO(ianh): Remove this once we roll stable in late 2021. -T? _ambiguate(T? value) => value; diff --git a/packages/camera/camera/example/lib/readme_full_example.dart b/packages/camera/camera/example/lib/readme_full_example.dart new file mode 100644 index 000000000000..a3c232ec44f7 --- /dev/null +++ b/packages/camera/camera/example/lib/readme_full_example.dart @@ -0,0 +1,69 @@ +// 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. + +// #docregion FullAppExample +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; + +late List _cameras; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + _cameras = await availableCameras(); + runApp(const CameraApp()); +} + +/// CameraApp is the Main Application. +class CameraApp extends StatefulWidget { + /// Default Constructor + const CameraApp({Key? key}) : super(key: key); + + @override + State createState() => _CameraAppState(); +} + +class _CameraAppState extends State { + late CameraController controller; + + @override + void initState() { + super.initState(); + controller = CameraController(_cameras[0], ResolutionPreset.max); + controller.initialize().then((_) { + if (!mounted) { + return; + } + setState(() {}); + }).catchError((Object e) { + if (e is CameraException) { + switch (e.code) { + case 'CameraAccessDenied': + print('User denied camera access.'); + break; + default: + print('Handle other errors.'); + break; + } + } + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!controller.value.isInitialized) { + return Container(); + } + return MaterialApp( + home: CameraPreview(controller), + ); + } +} +// #enddocregion FullAppExample diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index 1700074f1f88..e63024076fef 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=3.0.0" dependencies: camera: @@ -20,6 +20,7 @@ dependencies: video_player: ^2.1.4 dev_dependencies: + build_runner: ^2.1.10 flutter_driver: sdk: flutter flutter_test: diff --git a/packages/camera/camera/example/test/main_test.dart b/packages/camera/camera/example/test/main_test.dart new file mode 100644 index 000000000000..6e909efcfc62 --- /dev/null +++ b/packages/camera/camera/example/test/main_test.dart @@ -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. + +import 'package:camera_example/main.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Test snackbar', (WidgetTester tester) async { + WidgetsFlutterBinding.ensureInitialized(); + await tester.pumpWidget(const CameraApp()); + await tester.pumpAndSettle(); + expect(find.byType(SnackBar), findsOneWidget); + }); +} diff --git a/packages/camera/camera/ios/Classes/FLTCam_Test.h b/packages/camera/camera/ios/Classes/FLTCam_Test.h deleted file mode 100644 index db885d0d0773..000000000000 --- a/packages/camera/camera/ios/Classes/FLTCam_Test.h +++ /dev/null @@ -1,25 +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 "FLTCam.h" -#import "FLTSavePhotoDelegate.h" - -// APIs exposed for unit testing. -@interface FLTCam () - -/// The output for video capturing. -@property(readonly, nonatomic) AVCaptureVideoDataOutput *captureVideoOutput; - -/// The output for photo capturing. Exposed setter for unit tests. -@property(strong, nonatomic) AVCapturePhotoOutput *capturePhotoOutput API_AVAILABLE(ios(10)); - -/// A dictionary to retain all in-progress FLTSavePhotoDelegates. The key of the dictionary is the -/// AVCapturePhotoSettings's uniqueID for each photo capture operation, and the value is the -/// FLTSavePhotoDelegate that handles the result of each photo capture operation. Note that photo -/// capture operations may overlap, so we have to keep track of multiple delegates in progress, -/// instead of just a single delegate reference. -@property(readonly, nonatomic) - NSMutableDictionary *inProgressSavePhotoDelegates; - -@end diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 30e6221697c9..ed1c951925d8 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -5,14 +5,13 @@ import 'dart:async'; import 'dart:math'; -import 'package:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:quiver/core.dart'; -const MethodChannel _channel = MethodChannel('plugins.flutter.io/camera'); +import '../camera.dart'; /// Signature for a callback receiving the a camera image. /// @@ -257,7 +256,7 @@ class CameraController extends ValueNotifier { int _cameraId = kUninitializedCameraId; bool _isDisposed = false; - StreamSubscription? _imageStreamSubscription; + StreamSubscription? _imageStreamSubscription; FutureOr? _initCalled; StreamSubscription? _deviceOrientationSubscription; @@ -283,7 +282,7 @@ class CameraController extends ValueNotifier { ); } try { - final Completer _initializeCompleter = + final Completer initializeCompleter = Completer(); _deviceOrientationSubscription = CameraPlatform.instance @@ -304,7 +303,7 @@ class CameraController extends ValueNotifier { .onCameraInitialized(_cameraId) .first .then((CameraInitializedEvent event) { - _initializeCompleter.complete(event); + initializeCompleter.complete(event); })); await CameraPlatform.instance.initializeCamera( @@ -314,18 +313,18 @@ class CameraController extends ValueNotifier { value = value.copyWith( isInitialized: true, - previewSize: await _initializeCompleter.future + previewSize: await initializeCompleter.future .then((CameraInitializedEvent event) => Size( event.previewWidth, event.previewHeight, )), - exposureMode: await _initializeCompleter.future + exposureMode: await initializeCompleter.future .then((CameraInitializedEvent event) => event.exposureMode), - focusMode: await _initializeCompleter.future + focusMode: await initializeCompleter.future .then((CameraInitializedEvent event) => event.focusMode), - exposurePointSupported: await _initializeCompleter.future.then( + exposurePointSupported: await initializeCompleter.future.then( (CameraInitializedEvent event) => event.exposurePointSupported), - focusPointSupported: await _initializeCompleter.future + focusPointSupported: await initializeCompleter.future .then((CameraInitializedEvent event) => event.focusPointSupported), ); } on PlatformException catch (e) { @@ -359,8 +358,8 @@ class CameraController extends ValueNotifier { await CameraPlatform.instance.pausePreview(_cameraId); value = value.copyWith( isPreviewPaused: true, - previewPauseOrientation: - Optional.of(value.deviceOrientation)); + previewPauseOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -438,20 +437,15 @@ class CameraController extends ValueNotifier { } try { - await _channel.invokeMethod('startImageStream'); + _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); } - const EventChannel cameraEventChannel = - EventChannel('plugins.flutter.io/camera/imageStream'); - _imageStreamSubscription = - cameraEventChannel.receiveBroadcastStream().listen( - (dynamic imageData) { - onAvailable( - CameraImage.fromPlatformData(imageData as Map)); - }, - ); } /// Stop streaming images from platform camera. @@ -480,13 +474,11 @@ class CameraController extends ValueNotifier { try { value = value.copyWith(isStreamingImages: false); - await _channel.invokeMethod('stopImageStream'); + await _imageStreamSubscription?.cancel(); + _imageStreamSubscription = null; } on PlatformException catch (e) { throw CameraException(e.code, e.message); } - - await _imageStreamSubscription?.cancel(); - _imageStreamSubscription = null; } /// Start a video recording. @@ -513,7 +505,7 @@ class CameraController extends ValueNotifier { value = value.copyWith( isRecordingVideo: true, isRecordingPaused: false, - recordingOrientation: Optional.fromNullable( + recordingOrientation: Optional.of( value.lockedCaptureOrientation ?? value.deviceOrientation)); } on PlatformException catch (e) { throw CameraException(e.code, e.message); @@ -755,7 +747,7 @@ class CameraController extends ValueNotifier { await CameraPlatform.instance.lockCaptureOrientation( _cameraId, orientation ?? value.deviceOrientation); value = value.copyWith( - lockedCaptureOrientation: Optional.fromNullable( + lockedCaptureOrientation: Optional.of( orientation ?? value.deviceOrientation)); } on PlatformException catch (e) { throw CameraException(e.code, e.message); diff --git a/packages/camera/camera/lib/src/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart index fd3a3d6233bc..bfcad6626dd6 100644 --- a/packages/camera/camera/lib/src/camera_image.dart +++ b/packages/camera/camera/lib/src/camera_image.dart @@ -2,17 +2,31 @@ // 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'; -import 'package:flutter/material.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?, @@ -44,6 +58,12 @@ class Plane { /// 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. @@ -59,6 +79,8 @@ class ImageFormat { 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) { @@ -95,7 +117,19 @@ ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) { /// Although not all image formats are planar on iOS, we treat 1-dimensional /// images as single planar images. class CameraImage { - /// CameraImage Constructor + /// 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, diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index a9b3f2143b49..d8eadd8c93ae 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -2,15 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import '../camera.dart'; + /// A widget showing a live camera preview. class CameraPreview extends StatelessWidget { /// Creates a preview widget for the given camera controller. - const CameraPreview(this.controller, {this.child}); + const CameraPreview(this.controller, {Key? key, this.child}) + : super(key: key); /// The controller for the camera that the preview is shown for. final CameraController controller; diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 7bc0e452bb29..0f75d10c36cd 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,26 +4,27 @@ 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.9.4+14 +version: 0.10.0+4 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=3.0.0" flutter: plugin: platforms: android: - package: io.flutter.plugins.camera - pluginClass: CameraPlugin + default_package: camera_android ios: - pluginClass: CameraPlugin + default_package: camera_avfoundation web: default_package: camera_web dependencies: - camera_platform_interface: ^2.1.0 - camera_web: ^0.2.1 + camera_android: ^0.10.0 + camera_avfoundation: ^0.9.7+1 + camera_platform_interface: ^2.2.0 + camera_web: ^0.3.0 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.2 diff --git a/packages/camera/camera/test/camera_image_stream_test.dart b/packages/camera/camera/test/camera_image_stream_test.dart index 7055b2239a5a..a9320e46dfb5 100644 --- a/packages/camera/camera/test/camera_image_stream_test.dart +++ b/packages/camera/camera/test/camera_image_stream_test.dart @@ -2,18 +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:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'camera_test.dart'; -import 'utils/method_channel_mock.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); + late MockStreamingCameraPlatform mockPlatform; setUp(() { - CameraPlatform.instance = MockCameraPlatform(); + mockPlatform = MockStreamingCameraPlatform(); + CameraPlatform.instance = mockPlatform; }); test('startImageStream() throws $CameraException when uninitialized', () { @@ -87,13 +90,6 @@ void main() { }); test('startImageStream() calls CameraPlatform', () async { - final MethodChannelMock cameraChannelMock = MethodChannelMock( - channelName: 'plugins.flutter.io/camera', - methods: {'startImageStream': {}}); - final MethodChannelMock streamChannelMock = MethodChannelMock( - channelName: 'plugins.flutter.io/camera/imageStream', - methods: {'listen': {}}); - final CameraController cameraController = CameraController( const CameraDescription( name: 'cam', @@ -104,10 +100,8 @@ void main() { await cameraController.startImageStream((CameraImage image) => null); - expect(cameraChannelMock.log, - [isMethodCall('startImageStream', arguments: null)]); - expect(streamChannelMock.log, - [isMethodCall('listen', arguments: null)]); + expect(mockPlatform.streamCallLog, + ['onStreamedFrameAvailable', 'listen']); }); test('stopImageStream() throws $CameraException when uninitialized', () { @@ -178,19 +172,6 @@ void main() { }); test('stopImageStream() intended behaviour', () async { - final MethodChannelMock cameraChannelMock = MethodChannelMock( - channelName: 'plugins.flutter.io/camera', - methods: { - 'startImageStream': {}, - 'stopImageStream': {} - }); - final MethodChannelMock streamChannelMock = MethodChannelMock( - channelName: 'plugins.flutter.io/camera/imageStream', - methods: { - 'listen': {}, - 'cancel': {} - }); - final CameraController cameraController = CameraController( const CameraDescription( name: 'cam', @@ -201,14 +182,33 @@ void main() { await cameraController.startImageStream((CameraImage image) => null); await cameraController.stopImageStream(); - expect(cameraChannelMock.log, [ - isMethodCall('startImageStream', arguments: null), - isMethodCall('stopImageStream', arguments: null) - ]); - - expect(streamChannelMock.log, [ - isMethodCall('listen', arguments: null), - isMethodCall('cancel', arguments: null) - ]); + expect(mockPlatform.streamCallLog, + ['onStreamedFrameAvailable', 'listen', 'cancel']); }); } + +class MockStreamingCameraPlatform extends MockCameraPlatform { + List streamCallLog = []; + + StreamController? _streamController; + + @override + Stream onStreamedFrameAvailable(int cameraId, + {CameraImageStreamOptions? options}) { + streamCallLog.add('onStreamedFrameAvailable'); + _streamController = StreamController( + onListen: _onFrameStreamListen, + onCancel: _onFrameStreamCancel, + ); + return _streamController!.stream; + } + + void _onFrameStreamListen() { + streamCallLog.add('listen'); + } + + FutureOr _onFrameStreamCancel() async { + streamCallLog.add('cancel'); + _streamController = null; + } +} diff --git a/packages/camera/camera/test/camera_image_test.dart b/packages/camera/camera/test/camera_image_test.dart index b09a14177121..ecf4b509e2e4 100644 --- a/packages/camera/camera/test/camera_image_test.dart +++ b/packages/camera/camera/test/camera_image_test.dart @@ -2,16 +2,69 @@ // 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/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - group('$CameraImage tests', () { + test('translates correctly from platform interface classes', () { + final CameraImageData originalImage = CameraImageData( + format: const CameraImageFormat(ImageFormatGroup.jpeg, raw: 1234), + planes: [ + CameraImagePlane( + bytes: Uint8List.fromList([1, 2, 3, 4]), + bytesPerRow: 20, + bytesPerPixel: 3, + width: 200, + height: 100, + ), + CameraImagePlane( + bytes: Uint8List.fromList([5, 6, 7, 8]), + bytesPerRow: 18, + bytesPerPixel: 4, + width: 220, + height: 110, + ), + ], + width: 640, + height: 480, + lensAperture: 2.5, + sensorExposureTime: 5, + sensorSensitivity: 1.3, + ); + + final CameraImage image = CameraImage.fromPlatformInterface(originalImage); + // Simple values. + expect(image.width, 640); + expect(image.height, 480); + expect(image.lensAperture, 2.5); + expect(image.sensorExposureTime, 5); + expect(image.sensorSensitivity, 1.3); + // Format. + expect(image.format.group, ImageFormatGroup.jpeg); + expect(image.format.raw, 1234); + // Planes. + expect(image.planes.length, originalImage.planes.length); + for (int i = 0; i < image.planes.length; i++) { + expect( + image.planes[i].bytes.length, originalImage.planes[i].bytes.length); + for (int j = 0; j < image.planes[i].bytes.length; j++) { + expect(image.planes[i].bytes[j], originalImage.planes[i].bytes[j]); + } + expect( + image.planes[i].bytesPerPixel, originalImage.planes[i].bytesPerPixel); + expect(image.planes[i].bytesPerRow, originalImage.planes[i].bytesPerRow); + expect(image.planes[i].width, originalImage.planes[i].width); + expect(image.planes[i].height, originalImage.planes[i].height); + } + }); + + group('legacy constructors', () { test('$CameraImage can be created', () { debugDefaultTargetPlatformOverride = TargetPlatform.android; final CameraImage cameraImage = diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 76bfe40605d7..bedb0ea8e01f 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -203,7 +201,6 @@ void main() { controller.value = controller.value.copyWith( isInitialized: true, deviceOrientation: DeviceOrientation.portraitUp, - lockedCaptureOrientation: null, 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 c4e0c9388231..3c12648f13b9 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:math'; -import 'dart:ui'; import 'package:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; @@ -14,6 +13,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:quiver/core.dart'; List get mockAvailableCameras => [ const CameraDescription( @@ -697,7 +697,6 @@ void main() { PlatformException( code: 'TEST_ERROR', message: 'This is a test error message', - details: null, ), ); @@ -742,7 +741,6 @@ void main() { PlatformException( code: 'TEST_ERROR', message: 'This is a test error message', - details: null, ), ); @@ -787,7 +785,6 @@ void main() { PlatformException( code: 'TEST_ERROR', message: 'This is a test error message', - details: null, ), ); @@ -1181,6 +1178,30 @@ void main() { expect(cameraController.value.isPreviewPaused, equals(true)); }); + test( + 'pausePreview() sets previewPauseOrientation according to locked orientation', + () async { + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = cameraController.value.copyWith( + isPreviewPaused: false, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: + Optional.of(DeviceOrientation.landscapeRight)); + + await cameraController.pausePreview(); + + expect(cameraController.value.deviceOrientation, + equals(DeviceOrientation.portraitUp)); + expect(cameraController.value.previewPauseOrientation, + equals(DeviceOrientation.landscapeRight)); + }); + test('pausePreview() throws $CameraException on $PlatformException', () async { final CameraController cameraController = CameraController( @@ -1195,7 +1216,6 @@ void main() { PlatformException( code: 'TEST_ERROR', message: 'This is a test error message', - details: null, ), ); @@ -1261,7 +1281,6 @@ void main() { PlatformException( code: 'TEST_ERROR', message: 'This is a test error message', - details: null, ), ); @@ -1315,7 +1334,6 @@ void main() { PlatformException( code: 'TEST_ERROR', message: 'This is a test error message', - details: null, ), ); @@ -1361,7 +1379,6 @@ void main() { PlatformException( code: 'TEST_ERROR', message: 'This is a test error message', - details: null, ), ); diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index 62df1fd1a2a1..37168dbd48d7 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -2,10 +2,13 @@ // 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#106316) +// ignore: unnecessary_import import 'dart:ui'; import 'package:camera/camera.dart'; -import 'package:camera_platform_interface/camera_platform_interface.dart'; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -15,7 +18,6 @@ void main() { test('Can be created', () { const CameraValue cameraValue = CameraValue( isInitialized: false, - errorDescription: null, previewSize: Size(10, 10), isRecordingPaused: false, isRecordingVideo: false, @@ -29,7 +31,6 @@ void main() { lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, focusPointSupported: true, - isPreviewPaused: false, previewPauseOrientation: DeviceOrientation.portraitUp, ); @@ -126,7 +127,6 @@ void main() { test('toString() works as expected', () { const CameraValue cameraValue = CameraValue( isInitialized: false, - errorDescription: null, previewSize: Size(10, 10), isRecordingPaused: false, isRecordingVideo: false, diff --git a/packages/camera/camera_android/AUTHORS b/packages/camera/camera_android/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/camera/camera_android/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md new file mode 100644 index 000000000000..a62d3169e409 --- /dev/null +++ b/packages/camera/camera_android/CHANGELOG.md @@ -0,0 +1,41 @@ +## 0.10.0+4 + +* Upgrades `androidx.annotation` version to 1.5.0. + +## 0.10.0+3 + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. + +## 0.10.0+2 + +* Removes call to `join` on the camera's background `HandlerThread`. +* Updates minimum Flutter version to 2.10. + +## 0.10.0+1 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 0.10.0 + +* **Breaking Change** Updates Android camera access permission error codes to be consistent with other platforms. If your app still handles the legacy `cameraPermission` exception, please update it to handle the new permission exception codes that are noted in the README. +* Ignores missing return warnings in preparation for [upcoming analysis changes](https://github.com/flutter/flutter/issues/105750). + +## 0.9.8+3 + +* Skips duplicate calls to stop background thread and removes unnecessary closings of camera capture sessions on Android. + +## 0.9.8+2 + +* Fixes exception in registerWith caused by the switch to an in-package method channel. + +## 0.9.8+1 + +* Ignores deprecation warnings for upcoming styleFrom button API changes. + +## 0.9.8 + +* Switches to internal method channel implementation. + +## 0.9.7+1 + +* Splits from `camera` as a federated implementation. diff --git a/packages/camera/camera_android/LICENSE b/packages/camera/camera_android/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/camera/camera_android/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/camera/camera_android/README.md b/packages/camera/camera_android/README.md new file mode 100644 index 000000000000..de8897c1727a --- /dev/null +++ b/packages/camera/camera_android/README.md @@ -0,0 +1,11 @@ +# camera\_android + +The Android implementation of [`camera`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `camera` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/camera +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera_android/android/build.gradle similarity index 81% rename from packages/camera/camera/android/build.gradle rename to packages/camera/camera_android/android/build.gradle index 264f7f8edc7d..4fbb2270b556 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera_android/android/build.gradle @@ -35,6 +35,8 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { + // TODO(stuartmorgan): Enable when gradle is updated. + // disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' baseline file("lint-baseline.xml") @@ -59,9 +61,9 @@ android { } dependencies { - compileOnly 'androidx.annotation:annotation:1.1.0' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:4.0.0' - testImplementation 'androidx.test:core:1.3.0' + implementation 'androidx.annotation:annotation:1.5.0' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-inline:4.7.0' + testImplementation 'androidx.test:core:1.4.0' testImplementation 'org.robolectric:robolectric:4.5' } diff --git a/packages/camera/camera/android/lint-baseline.xml b/packages/camera/camera_android/android/lint-baseline.xml similarity index 100% rename from packages/camera/camera/android/lint-baseline.xml rename to packages/camera/camera_android/android/lint-baseline.xml diff --git a/packages/camera/camera_android/android/settings.gradle b/packages/camera/camera_android/android/settings.gradle new file mode 100644 index 000000000000..94a1bae9d6cd --- /dev/null +++ b/packages/camera/camera_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'camera_android' diff --git a/packages/camera/camera/android/src/main/AndroidManifest.xml b/packages/camera/camera_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/camera/camera/android/src/main/AndroidManifest.xml rename to packages/camera/camera_android/android/src/main/AndroidManifest.xml diff --git a/packages/camera/camera/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 similarity index 94% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java index 6a70ea0d10ea..3d2df98b60da 100644 --- a/packages/camera/camera/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 @@ -79,6 +79,24 @@ interface ErrorCallback { void onError(String errorCode, String errorMessage); } +/** A mockable wrapper for CameraDevice calls. */ +interface CameraDeviceWrapper { + @NonNull + CaptureRequest.Builder createCaptureRequest(int templateType) throws CameraAccessException; + + @TargetApi(VERSION_CODES.P) + void createCaptureSession(SessionConfiguration config) throws CameraAccessException; + + @TargetApi(VERSION_CODES.LOLLIPOP) + void createCaptureSession( + @NonNull List outputs, + @NonNull CameraCaptureSession.StateCallback callback, + @Nullable Handler handler) + throws CameraAccessException; + + void close(); +} + class Camera implements CameraCaptureCallback.CameraCaptureStateListener, ImageReader.OnImageAvailableListener { @@ -114,7 +132,7 @@ class Camera /** An additional thread for running tasks that shouldn't block the UI. */ private HandlerThread backgroundHandlerThread; - private CameraDevice cameraDevice; + private CameraDeviceWrapper cameraDevice; private CameraCaptureSession captureSession; private ImageReader pictureImageReader; private ImageReader imageStreamReader; @@ -136,6 +154,44 @@ class Camera private MethodChannel.Result flutterResult; + /** A CameraDeviceWrapper implementation that forwards calls to a CameraDevice. */ + private class DefaultCameraDeviceWrapper implements CameraDeviceWrapper { + private final CameraDevice cameraDevice; + + private DefaultCameraDeviceWrapper(CameraDevice cameraDevice) { + this.cameraDevice = cameraDevice; + } + + @NonNull + @Override + public CaptureRequest.Builder createCaptureRequest(int templateType) + throws CameraAccessException { + return cameraDevice.createCaptureRequest(templateType); + } + + @TargetApi(VERSION_CODES.P) + @Override + public void createCaptureSession(SessionConfiguration config) throws CameraAccessException { + cameraDevice.createCaptureSession(config); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + @SuppressWarnings("deprecation") + @Override + public void createCaptureSession( + @NonNull List outputs, + @NonNull CameraCaptureSession.StateCallback callback, + @Nullable Handler handler) + throws CameraAccessException { + cameraDevice.createCaptureSession(outputs, callback, backgroundHandler); + } + + @Override + public void close() { + cameraDevice.close(); + } + } + public Camera( final Activity activity, final SurfaceTextureEntry flutterTexture, @@ -261,7 +317,7 @@ public void open(String imageFormatGroup) throws CameraAccessException { new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice device) { - cameraDevice = device; + cameraDevice = new DefaultCameraDeviceWrapper(device); try { startPreview(); dartMessenger.sendCameraInitializedEvent( @@ -326,8 +382,8 @@ public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { backgroundHandler); } - private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { + @VisibleForTesting + void createCaptureSession(int templateType, Surface... surfaces) throws CameraAccessException { createCaptureSession(templateType, null, surfaces); } @@ -335,7 +391,7 @@ private void createCaptureSession( int templateType, Runnable onSuccessCallback, Surface... surfaces) throws CameraAccessException { // Close any existing capture session. - closeCaptureSession(); + captureSession = null; // Create a new capture builder. previewRequestBuilder = cameraDevice.createCaptureRequest(templateType); @@ -584,7 +640,6 @@ public void onCaptureCompleted( try { captureSession.stopRepeating(); - captureSession.abortCaptures(); Log.i(TAG, "sending capture request"); captureSession.capture(stillBuilder.build(), captureCallback, backgroundHandler); } catch (CameraAccessException e) { @@ -616,11 +671,6 @@ public void startBackgroundThread() { public void stopBackgroundThread() { if (backgroundHandlerThread != null) { backgroundHandlerThread.quitSafely(); - try { - backgroundHandlerThread.join(); - } catch (InterruptedException e) { - dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); - } } backgroundHandlerThread = null; backgroundHandler = null; @@ -1118,12 +1168,19 @@ private void closeCaptureSession() { public void close() { Log.i(TAG, "close"); - closeCaptureSession(); if (cameraDevice != null) { cameraDevice.close(); cameraDevice = null; + + // Closing the CameraDevice without closing the CameraCaptureSession is recommended + // for quickly closing the camera: + // https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession#close() + captureSession = null; + } else { + closeCaptureSession(); } + if (pictureImageReader != null) { pictureImageReader.close(); pictureImageReader = null; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java similarity index 74% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java index 7d60e0fffa5c..4441751e19cf 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java @@ -23,8 +23,22 @@ 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; - private boolean ongoing = false; + @VisibleForTesting boolean ongoing = false; void requestPermissions( Activity activity, @@ -32,7 +46,9 @@ void requestPermissions( boolean enableAudio, ResultCallback callback) { if (ongoing) { - callback.onResult("cameraPermission", "Camera permission request ongoing"); + callback.onResult( + CAMERA_PERMISSIONS_REQUEST_ONGOING, CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE); + return; } if (!hasCameraPermission(activity) || (enableAudio && !hasAudioPermission(activity))) { permissionsRegistry.addListener( @@ -90,9 +106,9 @@ public boolean onRequestPermissionsResult(int id, String[] permissions, int[] gr alreadyCalled = true; if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { - callback.onResult("cameraPermission", "MediaRecorderCamera permission not granted"); + callback.onResult(CAMERA_ACCESS_DENIED, CAMERA_ACCESS_DENIED_MESSAGE); } else if (grantResults.length > 1 && grantResults[1] != PackageManager.PERMISSION_GRANTED) { - callback.onResult("cameraPermission", "MediaRecorderAudio permission not granted"); + callback.onResult(AUDIO_ACCESS_DENIED, AUDIO_ACCESS_DENIED_MESSAGE); } else { callback.onResult(null, null); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraState.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraState.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java similarity index 96% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index dc62fce524d3..e15078e66afc 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -64,8 +64,9 @@ enum CameraEventType { * the main thread. The handler is mainly supplied so it will be easier test this class. */ DartMessenger(BinaryMessenger messenger, long cameraId, @NonNull Handler handler) { - cameraChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); - deviceChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/device"); + cameraChannel = + new MethodChannel(messenger, "plugins.flutter.io/camera_android/camera" + cameraId); + deviceChannel = new MethodChannel(messenger, "plugins.flutter.io/camera_android/fromPlatform"); this.handler = handler; } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java similarity index 98% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 35cc2b081bae..38201e1136c9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -49,8 +49,9 @@ final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { this.permissionsRegistry = permissionsAdder; this.textureRegistry = textureRegistry; - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera"); - imageStreamChannel = new EventChannel(messenger, "plugins.flutter.io/camera/imageStream"); + methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera_android"); + imageStreamChannel = + new EventChannel(messenger, "plugins.flutter.io/camera_android/imageStream"); methodChannel.setMethodCallHandler(this); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Point.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/Point.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Point.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/Point.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java diff --git a/packages/camera/camera/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 similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java similarity index 93% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java index dd1e489e6225..ec6fa13dbd1d 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java @@ -142,26 +142,32 @@ public int getPhotoOrientation(PlatformChannel.DeviceOrientation orientation) { } /** - * Returns the device's video orientation in degrees based on the sensor orientation and the last - * known UI orientation. + * 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 degrees. + * @return The device's video orientation in clockwise degrees. */ public int getVideoOrientation() { return this.getVideoOrientation(this.lastOrientation); } /** - * Returns the device's video orientation in degrees based on the sensor orientation and the - * supplied {@link PlatformChannel.DeviceOrientation} value. + * 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 degrees. + * @return The device's video orientation in clockwise degrees. */ public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { int angle = 0; @@ -179,10 +185,10 @@ public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { angle = 180; break; case LANDSCAPE_LEFT: - angle = 90; + angle = 270; break; case LANDSCAPE_RIGHT: - angle = 270; + angle = 90; break; } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtils.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtils.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtils.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtils.java diff --git a/packages/camera/camera/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 similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java similarity index 100% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java rename to packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java new file mode 100644 index 000000000000..d734a63b15ca --- /dev/null +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java @@ -0,0 +1,75 @@ +// 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.camera; + +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.camera.CameraPermissions.CameraRequestPermissionsListener; +import io.flutter.plugins.camera.CameraPermissions.ResultCallback; +import org.junit.Test; + +public class CameraPermissionsTest { + @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."); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_getCameraBoundariesTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_getCameraBoundariesTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_getCameraBoundariesTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_getCameraBoundariesTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java similarity index 88% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index 1ed2e4c11d7b..9a679017ded2 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -18,19 +18,27 @@ import static org.mockito.Mockito.when; import android.app.Activity; +import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.SessionConfiguration; +import android.media.ImageReader; import android.media.MediaRecorder; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; +import android.util.Size; +import android.view.Surface; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.lifecycle.LifecycleObserver; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.camera.features.CameraFeatureFactory; +import io.flutter.plugins.camera.features.CameraFeatures; import io.flutter.plugins.camera.features.Point; import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; import io.flutter.plugins.camera.features.autofocus.FocusMode; @@ -50,11 +58,39 @@ import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; import io.flutter.plugins.camera.utils.TestUtils; import io.flutter.view.TextureRegistry; +import java.util.ArrayList; +import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; +class FakeCameraDeviceWrapper implements CameraDeviceWrapper { + final List captureRequests; + + FakeCameraDeviceWrapper(List captureRequests) { + this.captureRequests = captureRequests; + } + + @NonNull + @Override + public CaptureRequest.Builder createCaptureRequest(int var1) { + return captureRequests.remove(0); + } + + @Override + public void createCaptureSession(SessionConfiguration config) {} + + @Override + public void createCaptureSession( + @NonNull List outputs, + @NonNull CameraCaptureSession.StateCallback callback, + @Nullable Handler handler) {} + + @Override + public void close() {} +} + public class CameraTest { private CameraProperties mockCameraProperties; private CameraFeatureFactory mockCameraFeatureFactory; @@ -801,6 +837,84 @@ public void startBackgroundThread_shouldNotStartNewThreadWhenAlreadyCreated() { verify(mockHandlerThread, times(1)).start(); } + @Test + public void stopBackgroundThread_quitsSafely() throws InterruptedException { + camera.startBackgroundThread(); + camera.stopBackgroundThread(); + + verify(mockHandlerThread).quitSafely(); + verify(mockHandlerThread, never()).join(); + } + + @Test + public void onConverge_shouldTakePictureWithoutAbortingSession() throws CameraAccessException { + ArrayList mockRequestBuilders = new ArrayList<>(); + mockRequestBuilders.add(mock(CaptureRequest.Builder.class)); + CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders); + // Stub out other features used by the flow. + TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera); + TestUtils.setPrivateField(camera, "pictureImageReader", mock(ImageReader.class)); + SensorOrientationFeature mockSensorOrientationFeature = + mockCameraFeatureFactory.createSensorOrientationFeature(mockCameraProperties, null, null); + DeviceOrientationManager mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + when(mockSensorOrientationFeature.getDeviceOrientationManager()) + .thenReturn(mockDeviceOrientationManager); + + // Simulate a post-precapture flow. + camera.onConverged(); + // A picture should be taken. + verify(mockCaptureSession, times(1)).capture(any(), any(), any()); + // The session shuold not be aborted as part of this flow, as this breaks capture on some + // devices, and causes delays on others. + verify(mockCaptureSession, never()).abortCaptures(); + } + + @Test + public void createCaptureSession_doesNotCloseCaptureSession() throws CameraAccessException { + Surface mockSurface = mock(Surface.class); + SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class); + ResolutionFeature mockResolutionFeature = mock(ResolutionFeature.class); + Size mockSize = mock(Size.class); + ArrayList mockRequestBuilders = new ArrayList<>(); + mockRequestBuilders.add(mock(CaptureRequest.Builder.class)); + CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders); + TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera); + + TextureRegistry.SurfaceTextureEntry cameraFlutterTexture = + (TextureRegistry.SurfaceTextureEntry) TestUtils.getPrivateField(camera, "flutterTexture"); + CameraFeatures cameraFeatures = + (CameraFeatures) TestUtils.getPrivateField(camera, "cameraFeatures"); + ResolutionFeature resolutionFeature = + (ResolutionFeature) + TestUtils.getPrivateField(mockCameraFeatureFactory, "mockResolutionFeature"); + + when(cameraFlutterTexture.surfaceTexture()).thenReturn(mockSurfaceTexture); + when(resolutionFeature.getPreviewSize()).thenReturn(mockSize); + + camera.createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, mockSurface); + + verify(mockCaptureSession, never()).close(); + } + + @Test + public void close_doesCloseCaptureSessionWhenCameraDeviceNull() { + camera.close(); + + verify(mockCaptureSession).close(); + } + + @Test + public void close_doesNotCloseCaptureSessionWhenCameraDeviceNonNull() { + ArrayList mockRequestBuilders = new ArrayList<>(); + mockRequestBuilders.add(mock(CaptureRequest.Builder.class)); + CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders); + TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera); + + camera.close(); + + verify(mockCaptureSession, never()).close(); + } + private static class TestCameraFeatureFactory implements CameraFeatureFactory { private final AutoFocusFeature mockAutoFocusFeature; private final ExposureLockFeature mockExposureLockFeature; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java diff --git a/packages/camera/camera/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 similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java similarity index 94% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java index 82449a10188a..3762006f46d4 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java @@ -62,9 +62,9 @@ public void getVideoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); assertEquals(0, degreesPortraitUp); - assertEquals(90, degreesLandscapeLeft); + assertEquals(270, degreesLandscapeLeft); assertEquals(180, degreesPortraitDown); - assertEquals(270, degreesLandscapeRight); + assertEquals(90, degreesLandscapeRight); } @Test @@ -81,18 +81,30 @@ public void getVideoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft( orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); assertEquals(90, degreesPortraitUp); - assertEquals(180, degreesLandscapeLeft); + assertEquals(0, degreesLandscapeLeft); assertEquals(270, degreesPortraitDown); - assertEquals(0, degreesLandscapeRight); + assertEquals(180, degreesLandscapeRight); } @Test - public void getVideoOrientation_shouldFallbackToSensorOrientationWhenOrientationIsNull() { - setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + public void getVideoOrientation_fallbackToPortraitSensorOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); int degrees = deviceOrientationManager.getVideoOrientation(null); - assertEquals(90, degrees); + assertEquals(0, degrees); + } + + @Test + public void getVideoOrientation_fallbackToLandscapeSensorOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + DeviceOrientationManager orientationManager = + DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 90); + + int degrees = orientationManager.getVideoOrientation(null); + + assertEquals(0, degrees); } @Test diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java similarity index 100% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java rename to packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java diff --git a/packages/camera/camera/android/src/test/resources/robolectric.properties b/packages/camera/camera_android/android/src/test/resources/robolectric.properties similarity index 100% rename from packages/camera/camera/android/src/test/resources/robolectric.properties rename to packages/camera/camera_android/android/src/test/resources/robolectric.properties diff --git a/packages/camera/camera_android/example/android/app/build.gradle b/packages/camera/camera_android/example/android/app/build.gradle new file mode 100644 index 000000000000..5d6af5887012 --- /dev/null +++ b/packages/camera/camera_android/example/android/app/build.gradle @@ -0,0 +1,64 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.cameraexample" + minSdkVersion 21 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + profile { + matchingFallbacks = ['debug', 'release'] + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} diff --git a/packages/flutter_plugin_android_lifecycle/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera_android/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 92% rename from packages/flutter_plugin_android_lifecycle/android/gradle/wrapper/gradle-wrapper.properties rename to packages/camera/camera_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index d757f3d33fcc..29e413457635 100644 --- a/packages/flutter_plugin_android_lifecycle/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/camera/camera_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/packages/camera/camera_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/camera/camera_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/camera/camera_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -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. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/camera/camera_android/example/android/app/src/androidTest/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java b/packages/camera/camera_android/example/android/app/src/androidTest/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java new file mode 100644 index 000000000000..39cae489d9fa --- /dev/null +++ b/packages/camera/camera_android/example/android/app/src/androidTest/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.cameraexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/camera/camera_android/example/android/app/src/main/AndroidManifest.xml b/packages/camera/camera_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..cef23162ddb6 --- /dev/null +++ b/packages/camera/camera_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/camera/camera_android/example/android/app/src/main/res/drawable/launch_background.xml b/packages/camera/camera_android/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/camera/camera_android/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/camera/camera_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/camera/camera_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/camera/camera_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/camera/camera_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/camera/camera_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/camera/camera_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/camera/camera_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/camera/camera_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/camera/camera_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/camera/camera_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/camera/camera_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/camera/camera_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/camera/camera_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/camera/camera_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/camera/camera_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/camera/camera_android/example/android/app/src/main/res/values/styles.xml b/packages/camera/camera_android/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..00fa4417cfbe --- /dev/null +++ b/packages/camera/camera_android/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/camera/camera_android/example/android/build.gradle b/packages/camera/camera_android/example/android/build.gradle new file mode 100644 index 000000000000..c21bff8e0a2f --- /dev/null +++ b/packages/camera/camera_android/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.0.1' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/camera/camera_android/example/android/gradle.properties b/packages/camera/camera_android/example/android/gradle.properties new file mode 100644 index 000000000000..b253d8e5f746 --- /dev/null +++ b/packages/camera/camera_android/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=false +android.enableR8=true diff --git a/packages/in_app_purchase/in_app_purchase_android/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera_android/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 80% rename from packages/in_app_purchase/in_app_purchase_android/android/gradle/wrapper/gradle-wrapper.properties rename to packages/camera/camera_android/example/android/gradle/wrapper/gradle-wrapper.properties index baf2285f8c53..297f2fec363f 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/camera/camera_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Mon Oct 29 10:30:44 PDT 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/camera/camera_android/example/android/settings.gradle b/packages/camera/camera_android/example/android/settings.gradle new file mode 100644 index 000000000000..115da6cb4f4d --- /dev/null +++ b/packages/camera/camera_android/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withInputStream { stream -> plugins.load(stream) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/packages/camera/camera_android/example/integration_test/camera_test.dart b/packages/camera/camera_android/example/integration_test/camera_test.dart new file mode 100644 index 000000000000..3b1aae6aec51 --- /dev/null +++ b/packages/camera/camera_android/example/integration_test/camera_test.dart @@ -0,0 +1,248 @@ +// 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 'dart:ui'; + +import 'package:camera_android/camera_android.dart'; +import 'package:camera_example/camera_controller.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + late Directory testDir; + + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + CameraPlatform.instance = AndroidCamera(); + final Directory extDir = await getTemporaryDirectory(); + testDir = await Directory('${extDir.path}/test').create(recursive: true); + }); + + tearDownAll(() async { + await testDir.delete(recursive: true); + }); + + final Map presetExpectedSizes = + { + ResolutionPreset.low: const Size(240, 320), + ResolutionPreset.medium: const Size(480, 720), + ResolutionPreset.high: const Size(720, 1280), + ResolutionPreset.veryHigh: const Size(1080, 1920), + ResolutionPreset.ultraHigh: const Size(2160, 3840), + // Don't bother checking for max here since it could be anything. + }; + + /// Verify that [actual] has dimensions that are at least as large as + /// [expectedSize]. Allows for a mismatch in portrait vs landscape. Returns + /// whether the dimensions exactly match. + bool assertExpectedDimensions(Size expectedSize, Size actual) { + expect(actual.shortestSide, lessThanOrEqualTo(expectedSize.shortestSide)); + expect(actual.longestSide, lessThanOrEqualTo(expectedSize.longestSide)); + return actual.shortestSide == expectedSize.shortestSide && + actual.longestSide == expectedSize.longestSide; + } + + // This tests that the capture is no bigger than the preset, since we have + // automatic code to fall back to smaller sizes when we need to. Returns + // whether the image is exactly the desired resolution. + Future testCaptureImageResolution( + CameraController controller, ResolutionPreset preset) async { + final Size expectedSize = presetExpectedSizes[preset]!; + print( + 'Capturing photo at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); + + // Take Picture + final XFile file = await controller.takePicture(); + + // Load picture + final File fileImage = File(file.path); + final Image image = await decodeImageFromList(fileImage.readAsBytesSync()); + + // Verify image dimensions are as expected + expect(image, isNotNull); + return assertExpectedDimensions( + expectedSize, Size(image.height.toDouble(), image.width.toDouble())); + } + + testWidgets( + 'Capture specific image resolutions', + (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + return; + } + for (final CameraDescription cameraDescription in cameras) { + bool previousPresetExactlySupported = true; + for (final MapEntry preset + in presetExpectedSizes.entries) { + final CameraController controller = + CameraController(cameraDescription, preset.key); + await controller.initialize(); + final bool presetExactlySupported = + await testCaptureImageResolution(controller, preset.key); + assert(!(!previousPresetExactlySupported && presetExactlySupported), + 'The camera took higher resolution pictures at a lower resolution.'); + previousPresetExactlySupported = presetExactlySupported; + await controller.dispose(); + } + } + }, + // TODO(egarciad): Fix https://github.com/flutter/flutter/issues/93686. + skip: true, + ); + + // This tests that the capture is no bigger than the preset, since we have + // automatic code to fall back to smaller sizes when we need to. Returns + // whether the image is exactly the desired resolution. + Future testCaptureVideoResolution( + CameraController controller, ResolutionPreset preset) async { + final Size expectedSize = presetExpectedSizes[preset]!; + print( + 'Capturing video at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); + + // Take Video + await controller.startVideoRecording(); + sleep(const Duration(milliseconds: 300)); + final XFile file = await controller.stopVideoRecording(); + + // Load video metadata + final File videoFile = File(file.path); + final VideoPlayerController videoController = + VideoPlayerController.file(videoFile); + await videoController.initialize(); + final Size video = videoController.value.size; + + // Verify image dimensions are as expected + expect(video, isNotNull); + return assertExpectedDimensions( + expectedSize, Size(video.height, video.width)); + } + + testWidgets( + 'Capture specific video resolutions', + (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + return; + } + for (final CameraDescription cameraDescription in cameras) { + bool previousPresetExactlySupported = true; + for (final MapEntry preset + in presetExpectedSizes.entries) { + final CameraController controller = + CameraController(cameraDescription, preset.key); + await controller.initialize(); + await controller.prepareForVideoRecording(); + final bool presetExactlySupported = + await testCaptureVideoResolution(controller, preset.key); + assert(!(!previousPresetExactlySupported && presetExactlySupported), + 'The camera took higher resolution pictures at a lower resolution.'); + previousPresetExactlySupported = presetExactlySupported; + await controller.dispose(); + } + } + }, + // TODO(egarciad): Fix https://github.com/flutter/flutter/issues/93686. + skip: true, + ); + + testWidgets('Pause and resume video recording', (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + await controller.prepareForVideoRecording(); + + int startPause; + int timePaused = 0; + + await controller.startVideoRecording(); + final int recordingStart = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + + await controller.pauseVideoRecording(); + startPause = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + await controller.resumeVideoRecording(); + timePaused += DateTime.now().millisecondsSinceEpoch - startPause; + + sleep(const Duration(milliseconds: 500)); + + await controller.pauseVideoRecording(); + startPause = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + await controller.resumeVideoRecording(); + timePaused += DateTime.now().millisecondsSinceEpoch - startPause; + + sleep(const Duration(milliseconds: 500)); + + final XFile file = await controller.stopVideoRecording(); + final int recordingTime = + DateTime.now().millisecondsSinceEpoch - recordingStart; + + final File videoFile = File(file.path); + final VideoPlayerController videoController = VideoPlayerController.file( + videoFile, + ); + await videoController.initialize(); + final int duration = videoController.value.duration.inMilliseconds; + await videoController.dispose(); + + expect(duration, lessThan(recordingTime - timePaused)); + }); + + testWidgets( + 'image streaming', + (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + bool isDetecting = false; + + await controller.startImageStream((CameraImageData image) { + if (isDetecting) { + return; + } + + isDetecting = true; + + expectLater(image, isNotNull).whenComplete(() => isDetecting = false); + }); + + expect(controller.value.isStreamingImages, true); + + sleep(const Duration(milliseconds: 500)); + + await controller.stopImageStream(); + await controller.dispose(); + }, + ); +} diff --git a/packages/camera/camera_android/example/lib/camera_controller.dart b/packages/camera/camera_android/example/lib/camera_controller.dart new file mode 100644 index 000000000000..09441cc5449c --- /dev/null +++ b/packages/camera/camera_android/example/lib/camera_controller.dart @@ -0,0 +1,437 @@ +// 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'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:quiver/core.dart'; + +/// The state of a [CameraController]. +class CameraValue { + /// Creates a new camera controller state. + const CameraValue({ + required this.isInitialized, + this.previewSize, + required this.isRecordingVideo, + required this.isTakingPicture, + required this.isStreamingImages, + required this.isRecordingPaused, + required this.flashMode, + required this.exposureMode, + required this.focusMode, + required this.deviceOrientation, + this.lockedCaptureOrientation, + this.recordingOrientation, + this.isPreviewPaused = false, + this.previewPauseOrientation, + }); + + /// 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, + focusMode: FocusMode.auto, + 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; + + /// True when video recording is paused. + 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; + + /// The size of the preview in pixels. + /// + /// Is `null` until [isInitialized] is `true`. + final Size? previewSize; + + /// 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; + + /// 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, + 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, + previewSize: previewSize ?? this.previewSize, + isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo, + isTakingPicture: isTakingPicture ?? this.isTakingPicture, + isStreamingImages: isStreamingImages ?? this.isStreamingImages, + isRecordingPaused: isRecordingPaused ?? this.isRecordingPaused, + flashMode: flashMode ?? this.flashMode, + exposureMode: exposureMode ?? this.exposureMode, + focusMode: focusMode ?? this.focusMode, + 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, ' + 'previewSize: $previewSize, ' + 'isStreamingImages: $isStreamingImages, ' + 'flashMode: $flashMode, ' + 'exposureMode: $exposureMode, ' + 'focusMode: $focusMode, ' + 'deviceOrientation: $deviceOrientation, ' + 'lockedCaptureOrientation: $lockedCaptureOrientation, ' + 'recordingOrientation: $recordingOrientation, ' + 'isPreviewPaused: $isPreviewPaused, ' + 'previewPausedOrientation: $previewPauseOrientation)'; + } +} + +/// Controls a device camera. +/// +/// This is a stripped-down version of the app-facing controller to serve as a +/// utility for the example and integration tests. It wraps only the calls that +/// have state associated with them, to consolidate tracking of camera state +/// outside of the overall example code. +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; + + late int _cameraId; + + bool _isDisposed = false; + StreamSubscription? _imageStreamSubscription; + FutureOr? _initCalled; + StreamSubscription? + _deviceOrientationSubscription; + + /// The camera identifier with which the controller is associated. + int get cameraId => _cameraId; + + /// Initializes the camera on the device. + Future initialize() async { + 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, + ); + + 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), + ); + + _initCalled = true; + } + + /// Prepare the capture session for video recording. + Future prepareForVideoRecording() async { + await CameraPlatform.instance.prepareForVideoRecording(); + } + + /// Pauses the current camera preview + Future pausePreview() async { + await CameraPlatform.instance.pausePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: true, + 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, + previewPauseOrientation: const Optional.absent()); + } + + /// Captures an image and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture fails. + Future takePicture() async { + value = value.copyWith(isTakingPicture: true); + final XFile file = await CameraPlatform.instance.takePicture(_cameraId); + value = value.copyWith(isTakingPicture: false); + return file; + } + + /// Start streaming images from platform camera. + Future startImageStream( + Function(CameraImageData image) onAvailable) async { + _imageStreamSubscription = CameraPlatform.instance + .onStreamedFrameAvailable(_cameraId) + .listen((CameraImageData imageData) { + onAvailable(imageData); + }); + value = value.copyWith(isStreamingImages: true); + } + + /// Stop streaming images from platform camera. + Future stopImageStream() async { + value = value.copyWith(isStreamingImages: false); + await _imageStreamSubscription?.cancel(); + _imageStreamSubscription = null; + } + + /// Start a video recording. + /// + /// The video is returned as a [XFile] after calling [stopVideoRecording]. + /// Throws a [CameraException] if the capture fails. + Future startVideoRecording() async { + await CameraPlatform.instance.startVideoRecording(_cameraId); + value = value.copyWith( + isRecordingVideo: true, + isRecordingPaused: false, + recordingOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); + } + + /// Stops the video recording and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture failed. + Future stopVideoRecording() async { + final XFile file = + await CameraPlatform.instance.stopVideoRecording(_cameraId); + value = value.copyWith( + isRecordingVideo: false, + recordingOrientation: const Optional.absent(), + ); + return file; + } + + /// Pause video recording. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future pauseVideoRecording() async { + await CameraPlatform.instance.pauseVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: true); + } + + /// Resume video recording after pausing. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future resumeVideoRecording() async { + await CameraPlatform.instance.resumeVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: false); + } + + /// Returns a widget showing a live camera preview. + Widget buildPreview() { + return CameraPlatform.instance.buildPreview(_cameraId); + } + + /// Sets the flash mode for taking pictures. + Future setFlashMode(FlashMode mode) async { + await CameraPlatform.instance.setFlashMode(_cameraId, mode); + value = value.copyWith(flashMode: mode); + } + + /// Sets the exposure mode for taking pictures. + Future setExposureMode(ExposureMode mode) async { + await CameraPlatform.instance.setExposureMode(_cameraId, mode); + value = value.copyWith(exposureMode: mode); + } + + /// Sets the exposure offset for the selected camera. + Future setExposureOffset(double offset) async { + // Check if offset is in range + final List range = await Future.wait(>[ + CameraPlatform.instance.getMinExposureOffset(_cameraId), + CameraPlatform.instance.getMaxExposureOffset(_cameraId) + ]); + + // Round to the closest step if needed + final double stepSize = + await CameraPlatform.instance.getExposureOffsetStepSize(_cameraId); + 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; + } + + return CameraPlatform.instance.setExposureOffset(_cameraId, offset); + } + + /// Locks the capture orientation. + /// + /// If [orientation] is omitted, the current device orientation is used. + Future lockCaptureOrientation() async { + await CameraPlatform.instance + .lockCaptureOrientation(_cameraId, 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. + Future setFocusMode(FocusMode mode) async { + await CameraPlatform.instance.setFocusMode(_cameraId, mode); + value = value.copyWith(focusMode: mode); + } + + /// Releases the resources of this camera. + @override + Future dispose() async { + if (_isDisposed) { + return; + } + _deviceOrientationSubscription?.cancel(); + _isDisposed = true; + super.dispose(); + if (_initCalled != null) { + await _initCalled; + await CameraPlatform.instance.dispose(_cameraId); + } + } + + @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); + } + } +} diff --git a/packages/camera/camera_android/example/lib/camera_preview.dart b/packages/camera/camera_android/example/lib/camera_preview.dart new file mode 100644 index 000000000000..5e8f64cb2fbd --- /dev/null +++ b/packages/camera/camera_android/example/lib/camera_preview.dart @@ -0,0 +1,85 @@ +// 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, {Key? key, this.child}) + : super(key: key); + + /// 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) { + final double cameraAspectRatio = + controller.value.previewSize!.width / + controller.value.previewSize!.height; + return AspectRatio( + aspectRatio: _isLandscape() + ? cameraAspectRatio + : (1 / cameraAspectRatio), + 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/example/lib/main.dart b/packages/camera/camera_android/example/lib/main.dart new file mode 100644 index 000000000000..9ebc27e4be5b --- /dev/null +++ b/packages/camera/camera_android/example/lib/main.dart @@ -0,0 +1,1096 @@ +// 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'; +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/scheduler.dart'; +import 'package:video_player/video_player.dart'; + +import 'camera_controller.dart'; +import 'camera_preview.dart'; + +/// Camera example home widget. +class CameraExampleHome extends StatefulWidget { + /// Default Constructor + const CameraExampleHome({Key? key}) : super(key: key); + + @override + State createState() { + return _CameraExampleHomeState(); + } +} + +/// 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; + default: + throw ArgumentError('Unknown lens direction'); + } +} + +void _logError(String code, String? message) { + if (message != null) { + print('Error: $code\nError Message: $message'); + } else { + print('Error: $code'); + } +} + +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(); + _ambiguate(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() { + _ambiguate(WidgetsBinding.instance)?.removeObserver(this); + _flashModeControlRowAnimationController.dispose(); + _exposureModeControlRowAnimationController.dispose(); + super.dispose(); + } + + @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); + } + } + + @override + Widget build(BuildContext context) { + 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; + } + + _currentScale = (_baseScale * details.scale) + .clamp(_minAvailableZoom, _maxAvailableZoom); + + await CameraPlatform.instance + .setZoomLevel(controller!.cameraId, _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: controller != null ? onFlashModeButtonPressed : null, + ), + // The exposure and focus mode are currently not supported on the web. + ...!kIsWeb + ? [ + IconButton( + icon: const Icon(Icons.exposure), + color: Colors.blue, + onPressed: controller != null + ? onExposureModeButtonPressed + : null, + ), + IconButton( + icon: const Icon(Icons.filter_center_focus), + color: Colors.blue, + onPressed: + controller != null ? onFocusModeButtonPressed : null, + ) + ] + : [], + IconButton( + icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), + color: Colors.blue, + onPressed: controller != null ? onAudioModeButtonPressed : null, + ), + IconButton( + icon: Icon(controller?.value.isCaptureOrientationLocked ?? false + ? Icons.screen_lock_rotation + : Icons.screen_rotation), + color: Colors.blue, + onPressed: controller != null + ? onCaptureOrientationLockButtonPressed + : null, + ), + ], + ), + _flashModeControlRowWidget(), + _exposureModeControlRowWidget(), + _focusModeControlRowWidget(), + ], + ); + } + + Widget _flashModeControlRowWidget() { + return SizeTransition( + sizeFactor: _flashModeControlRowAnimation, + child: ClipRect( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon(Icons.flash_off), + color: controller?.value.flashMode == FlashMode.off + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.off) + : null, + ), + IconButton( + icon: const Icon(Icons.flash_auto), + color: controller?.value.flashMode == FlashMode.auto + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.auto) + : null, + ), + IconButton( + icon: const Icon(Icons.flash_on), + color: controller?.value.flashMode == FlashMode.always + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.always) + : null, + ), + IconButton( + icon: const Icon(Icons.highlight), + color: controller?.value.flashMode == FlashMode.torch + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.torch) + : null, + ), + ], + ), + ), + ); + } + + Widget _exposureModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + // 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: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.auto) + : null, + onLongPress: () { + if (controller != null) { + CameraPlatform.instance + .setExposurePoint(controller!.cameraId, null); + showInSnackBar('Resetting exposure point'); + } + }, + child: const Text('AUTO'), + ), + TextButton( + style: styleLocked, + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.locked) + : null, + child: const Text('LOCKED'), + ), + TextButton( + style: styleLocked, + onPressed: controller != null + ? () => controller!.setExposureOffset(0.0) + : null, + 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: controller != null + ? () => onSetFocusModeButtonPressed(FocusMode.auto) + : null, + onLongPress: () { + if (controller != null) { + CameraPlatform.instance + .setFocusPoint(controller!.cameraId, null); + } + showInSnackBar('Resetting focus point'); + }, + child: const Text('AUTO'), + ), + TextButton( + style: styleLocked, + onPressed: controller != null + ? () => onSetFocusModeButtonPressed(FocusMode.locked) + : null, + 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: cameraController != null && + cameraController.value.isInitialized && + !cameraController.value.isRecordingVideo + ? onTakePictureButtonPressed + : null, + ), + IconButton( + icon: const Icon(Icons.videocam), + color: Colors.blue, + onPressed: cameraController != null && + cameraController.value.isInitialized && + !cameraController.value.isRecordingVideo + ? onVideoRecordButtonPressed + : null, + ), + IconButton( + icon: cameraController != null && + (!cameraController.value.isRecordingVideo || + cameraController.value.isRecordingPaused) + ? const Icon(Icons.play_arrow) + : const Icon(Icons.pause), + color: Colors.blue, + onPressed: cameraController != null && + cameraController.value.isInitialized && + cameraController.value.isRecordingVideo + ? (cameraController.value.isRecordingPaused) + ? onResumeButtonPressed + : onPauseButtonPressed + : null, + ), + IconButton( + icon: const Icon(Icons.stop), + color: Colors.red, + onPressed: cameraController != null && + cameraController.value.isInitialized && + cameraController.value.isRecordingVideo + ? onStopButtonPressed + : null, + ), + 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) { + _ambiguate(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 Point point = Point( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ); + CameraPlatform.instance.setExposurePoint(cameraController.cameraId, point); + CameraPlatform.instance.setFocusPoint(cameraController.cameraId, point); + } + + 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(() {}); + } + }); + + try { + await cameraController.initialize(); + await Future.wait(>[ + // The exposure mode is currently not supported on the web. + ...!kIsWeb + ? >[ + CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId) + .then( + (double value) => _minAvailableExposureOffset = value), + CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId) + .then((double value) => _maxAvailableExposureOffset = value) + ] + : >[], + CameraPlatform.instance + .getMaxZoomLevel(cameraController.cameraId) + .then((double value) => _maxAvailableZoom = value), + CameraPlatform.instance + .getMinZoomLevel(cameraController.cameraId) + .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; + case 'cameraPermission': + // Android & web only + showInSnackBar('Unknown permission error.'); + 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({Key? key}) : super(key: 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 CameraPlatform.instance.availableCameras(); + } on CameraException catch (e) { + _logError(e.code, e.description); + } + runApp(const CameraApp()); +} + +/// 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. +// 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 new file mode 100644 index 000000000000..2e530e02ca71 --- /dev/null +++ b/packages/camera/camera_android/example/pubspec.yaml @@ -0,0 +1,34 @@ +name: camera_example +description: Demonstrates how to use the camera plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + camera_android: + # When depending on this package from a real application you should use: + # camera_android: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + camera_platform_interface: ^2.2.0 + flutter: + sdk: flutter + path_provider: ^2.0.0 + quiver: ^3.0.0 + video_player: ^2.1.4 + +dev_dependencies: + build_runner: ^2.1.10 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/camera/camera_android/example/test_driver/integration_test.dart b/packages/camera/camera_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4ec97e66d36c --- /dev/null +++ b/packages/camera/camera_android/example/test_driver/integration_test.dart @@ -0,0 +1,64 @@ +// 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'; + +import 'package:flutter_driver/flutter_driver.dart'; + +const String _examplePackage = 'io.flutter.plugins.cameraexample'; + +Future main() async { + if (!(Platform.isLinux || Platform.isMacOS)) { + print('This test must be run on a POSIX host. Skipping...'); + exit(0); + } + final bool adbExists = + Process.runSync('which', ['adb']).exitCode == 0; + if (!adbExists) { + print(r'This test needs ADB to exist on the $PATH. Skipping...'); + exit(0); + } + print('Granting camera permissions...'); + Process.runSync('adb', [ + 'shell', + 'pm', + 'grant', + _examplePackage, + 'android.permission.CAMERA' + ]); + Process.runSync('adb', [ + 'shell', + 'pm', + 'grant', + _examplePackage, + 'android.permission.RECORD_AUDIO' + ]); + print('Starting test.'); + final FlutterDriver driver = await FlutterDriver.connect(); + final String data = await driver.requestData( + null, + timeout: const Duration(minutes: 1), + ); + await driver.close(); + print('Test finished. Revoking camera permissions...'); + Process.runSync('adb', [ + 'shell', + 'pm', + 'revoke', + _examplePackage, + 'android.permission.CAMERA' + ]); + Process.runSync('adb', [ + 'shell', + 'pm', + 'revoke', + _examplePackage, + 'android.permission.RECORD_AUDIO' + ]); + + final Map result = jsonDecode(data) as Map; + exit(result['result'] == 'true' ? 0 : 1); +} diff --git a/packages/camera/camera_android/lib/camera_android.dart b/packages/camera/camera_android/lib/camera_android.dart new file mode 100644 index 000000000000..93e3e17290c0 --- /dev/null +++ b/packages/camera/camera_android/lib/camera_android.dart @@ -0,0 +1,5 @@ +// 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. + +export 'src/android_camera.dart'; diff --git a/packages/camera/camera_android/lib/src/android_camera.dart b/packages/camera/camera_android/lib/src/android_camera.dart new file mode 100644 index 000000000000..36077eac8eed --- /dev/null +++ b/packages/camera/camera_android/lib/src/android_camera.dart @@ -0,0 +1,589 @@ +// 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:math'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_transform/stream_transform.dart'; + +import 'type_conversion.dart'; +import 'utils.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/camera_android'); + +/// The Android implementation of [CameraPlatform] that uses method channels. +class AndroidCamera extends CameraPlatform { + /// Registers this class as the default instance of [CameraPlatform]. + static void registerWith() { + CameraPlatform.instance = AndroidCamera(); + } + + final Map _channels = {}; + + /// The name of the channel that device events from the platform side are + /// sent on. + @visibleForTesting + static const String deviceEventChannelName = + 'plugins.flutter.io/camera_android/fromPlatform'; + + /// The controller we need to broadcast the different events coming + /// from handleMethodCall, specific to camera events. + /// + /// It is a `broadcast` because multiple controllers will connect to + /// different stream views of this Controller. + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + final StreamController cameraEventStreamController = + StreamController.broadcast(); + + /// The controller we need to broadcast the different events coming + /// from handleMethodCall, specific to general device events. + /// + /// It is a `broadcast` because multiple controllers will connect to + /// different stream views of this Controller. + late final StreamController _deviceEventStreamController = + _createDeviceEventStreamController(); + + StreamController _createDeviceEventStreamController() { + // Set up the method handler lazily. + const MethodChannel channel = MethodChannel(deviceEventChannelName); + channel.setMethodCallHandler(_handleDeviceMethodCall); + return StreamController.broadcast(); + } + + // The stream to receive frames from the native code. + StreamSubscription? _platformImageStreamSubscription; + + // The stream for vending frames to platform interface clients. + StreamController? _frameStreamController; + + Stream _cameraEvents(int cameraId) => + cameraEventStreamController.stream + .where((CameraEvent event) => event.cameraId == cameraId); + + @override + Future> availableCameras() async { + try { + final List>? cameras = await _channel + .invokeListMethod>('availableCameras'); + + if (cameras == null) { + return []; + } + + return cameras.map((Map camera) { + return CameraDescription( + name: camera['name']! as String, + lensDirection: + parseCameraLensDirection(camera['lensFacing']! as String), + sensorOrientation: camera['sensorOrientation']! as int, + ); + }).toList(); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset? resolutionPreset, { + bool enableAudio = false, + }) async { + try { + final Map? reply = await _channel + .invokeMapMethod('create', { + 'cameraName': cameraDescription.name, + 'resolutionPreset': resolutionPreset != null + ? _serializeResolutionPreset(resolutionPreset) + : null, + 'enableAudio': enableAudio, + }); + + return reply!['cameraId']! as int; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future initializeCamera( + int cameraId, { + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) { + _channels.putIfAbsent(cameraId, () { + final MethodChannel channel = + MethodChannel('plugins.flutter.io/camera_android/camera$cameraId'); + channel.setMethodCallHandler( + (MethodCall call) => handleCameraMethodCall(call, cameraId)); + return channel; + }); + + final Completer completer = Completer(); + + onCameraInitialized(cameraId).first.then((CameraInitializedEvent value) { + completer.complete(); + }); + + _channel.invokeMapMethod( + 'initialize', + { + 'cameraId': cameraId, + 'imageFormatGroup': imageFormatGroup.name(), + }, + ).catchError( + // TODO(srawlins): This should return a value of the future's type. This + // will fail upcoming analysis checks with + // https://github.com/flutter/flutter/issues/105750. + // ignore: body_might_complete_normally_catch_error + (Object error, StackTrace stackTrace) { + if (error is! PlatformException) { + throw error; + } + completer.completeError( + CameraException(error.code, error.message), + stackTrace, + ); + }, + ); + + return completer.future; + } + + @override + Future dispose(int cameraId) async { + if (_channels.containsKey(cameraId)) { + final MethodChannel? cameraChannel = _channels[cameraId]; + cameraChannel?.setMethodCallHandler(null); + _channels.remove(cameraId); + } + + await _channel.invokeMethod( + 'dispose', + {'cameraId': cameraId}, + ); + } + + @override + Stream onCameraInitialized(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraResolutionChanged(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraClosing(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraError(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onVideoRecordedEvent(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onDeviceOrientationChanged() { + return _deviceEventStreamController.stream + .whereType(); + } + + @override + Future lockCaptureOrientation( + int cameraId, + DeviceOrientation orientation, + ) async { + await _channel.invokeMethod( + 'lockCaptureOrientation', + { + 'cameraId': cameraId, + 'orientation': serializeDeviceOrientation(orientation) + }, + ); + } + + @override + Future unlockCaptureOrientation(int cameraId) async { + await _channel.invokeMethod( + 'unlockCaptureOrientation', + {'cameraId': cameraId}, + ); + } + + @override + Future takePicture(int cameraId) async { + final String? path = await _channel.invokeMethod( + 'takePicture', + {'cameraId': cameraId}, + ); + + if (path == null) { + throw CameraException( + 'INVALID_PATH', + 'The platform "$defaultTargetPlatform" did not return a path while reporting success. The platform should always return a valid path or report an error.', + ); + } + + return XFile(path); + } + + @override + Future prepareForVideoRecording() => + _channel.invokeMethod('prepareForVideoRecording'); + + @override + Future startVideoRecording(int cameraId, + {Duration? maxVideoDuration}) async { + await _channel.invokeMethod( + 'startVideoRecording', + { + 'cameraId': cameraId, + 'maxVideoDuration': maxVideoDuration?.inMilliseconds, + }, + ); + } + + @override + Future stopVideoRecording(int cameraId) async { + final String? path = await _channel.invokeMethod( + 'stopVideoRecording', + {'cameraId': cameraId}, + ); + + if (path == null) { + throw CameraException( + 'INVALID_PATH', + 'The platform "$defaultTargetPlatform" did not return a path while reporting success. The platform should always return a valid path or report an error.', + ); + } + + return XFile(path); + } + + @override + Future pauseVideoRecording(int cameraId) => _channel.invokeMethod( + 'pauseVideoRecording', + {'cameraId': cameraId}, + ); + + @override + Future resumeVideoRecording(int cameraId) => + _channel.invokeMethod( + 'resumeVideoRecording', + {'cameraId': cameraId}, + ); + + @override + Stream onStreamedFrameAvailable(int cameraId, + {CameraImageStreamOptions? options}) { + _frameStreamController = StreamController( + onListen: _onFrameStreamListen, + onPause: _onFrameStreamPauseResume, + onResume: _onFrameStreamPauseResume, + onCancel: _onFrameStreamCancel, + ); + return _frameStreamController!.stream; + } + + void _onFrameStreamListen() { + _startPlatformStream(); + } + + Future _startPlatformStream() async { + await _channel.invokeMethod('startImageStream'); + const EventChannel cameraEventChannel = + EventChannel('plugins.flutter.io/camera_android/imageStream'); + _platformImageStreamSubscription = + cameraEventChannel.receiveBroadcastStream().listen((dynamic imageData) { + _frameStreamController! + .add(cameraImageFromPlatformData(imageData as Map)); + }); + } + + FutureOr _onFrameStreamCancel() async { + await _channel.invokeMethod('stopImageStream'); + await _platformImageStreamSubscription?.cancel(); + _platformImageStreamSubscription = null; + _frameStreamController = null; + } + + void _onFrameStreamPauseResume() { + throw CameraException('InvalidCall', + 'Pause and resume are not supported for onStreamedFrameAvailable'); + } + + @override + Future setFlashMode(int cameraId, FlashMode mode) => + _channel.invokeMethod( + 'setFlashMode', + { + 'cameraId': cameraId, + 'mode': _serializeFlashMode(mode), + }, + ); + + @override + Future setExposureMode(int cameraId, ExposureMode mode) => + _channel.invokeMethod( + 'setExposureMode', + { + 'cameraId': cameraId, + 'mode': serializeExposureMode(mode), + }, + ); + + @override + Future setExposurePoint(int cameraId, Point? point) { + assert(point == null || point.x >= 0 && point.x <= 1); + assert(point == null || point.y >= 0 && point.y <= 1); + + return _channel.invokeMethod( + 'setExposurePoint', + { + 'cameraId': cameraId, + 'reset': point == null, + 'x': point?.x, + 'y': point?.y, + }, + ); + } + + @override + Future getMinExposureOffset(int cameraId) async { + final double? minExposureOffset = await _channel.invokeMethod( + 'getMinExposureOffset', + {'cameraId': cameraId}, + ); + + return minExposureOffset!; + } + + @override + Future getMaxExposureOffset(int cameraId) async { + final double? maxExposureOffset = await _channel.invokeMethod( + 'getMaxExposureOffset', + {'cameraId': cameraId}, + ); + + return maxExposureOffset!; + } + + @override + Future getExposureOffsetStepSize(int cameraId) async { + final double? stepSize = await _channel.invokeMethod( + 'getExposureOffsetStepSize', + {'cameraId': cameraId}, + ); + + return stepSize!; + } + + @override + Future setExposureOffset(int cameraId, double offset) async { + final double? appliedOffset = await _channel.invokeMethod( + 'setExposureOffset', + { + 'cameraId': cameraId, + 'offset': offset, + }, + ); + + return appliedOffset!; + } + + @override + Future setFocusMode(int cameraId, FocusMode mode) => + _channel.invokeMethod( + 'setFocusMode', + { + 'cameraId': cameraId, + 'mode': serializeFocusMode(mode), + }, + ); + + @override + Future setFocusPoint(int cameraId, Point? point) { + assert(point == null || point.x >= 0 && point.x <= 1); + assert(point == null || point.y >= 0 && point.y <= 1); + + return _channel.invokeMethod( + 'setFocusPoint', + { + 'cameraId': cameraId, + 'reset': point == null, + 'x': point?.x, + 'y': point?.y, + }, + ); + } + + @override + Future getMaxZoomLevel(int cameraId) async { + final double? maxZoomLevel = await _channel.invokeMethod( + 'getMaxZoomLevel', + {'cameraId': cameraId}, + ); + + return maxZoomLevel!; + } + + @override + Future getMinZoomLevel(int cameraId) async { + final double? minZoomLevel = await _channel.invokeMethod( + 'getMinZoomLevel', + {'cameraId': cameraId}, + ); + + return minZoomLevel!; + } + + @override + Future setZoomLevel(int cameraId, double zoom) async { + try { + await _channel.invokeMethod( + 'setZoomLevel', + { + 'cameraId': cameraId, + 'zoom': zoom, + }, + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future pausePreview(int cameraId) async { + await _channel.invokeMethod( + 'pausePreview', + {'cameraId': cameraId}, + ); + } + + @override + Future resumePreview(int cameraId) async { + await _channel.invokeMethod( + 'resumePreview', + {'cameraId': cameraId}, + ); + } + + @override + Widget buildPreview(int cameraId) { + return Texture(textureId: cameraId); + } + + /// Returns the flash mode as a String. + String _serializeFlashMode(FlashMode flashMode) { + switch (flashMode) { + case FlashMode.off: + return 'off'; + case FlashMode.auto: + return 'auto'; + case FlashMode.always: + return 'always'; + case FlashMode.torch: + return 'torch'; + default: + throw ArgumentError('Unknown FlashMode value'); + } + } + + /// Returns the resolution preset as a String. + String _serializeResolutionPreset(ResolutionPreset resolutionPreset) { + switch (resolutionPreset) { + case ResolutionPreset.max: + return 'max'; + case ResolutionPreset.ultraHigh: + return 'ultraHigh'; + case ResolutionPreset.veryHigh: + return 'veryHigh'; + case ResolutionPreset.high: + return 'high'; + case ResolutionPreset.medium: + return 'medium'; + case ResolutionPreset.low: + return 'low'; + default: + throw ArgumentError('Unknown ResolutionPreset value'); + } + } + + /// Converts messages received from the native platform into device events. + Future _handleDeviceMethodCall(MethodCall call) async { + switch (call.method) { + case 'orientation_changed': + _deviceEventStreamController.add(DeviceOrientationChangedEvent( + deserializeDeviceOrientation( + call.arguments['orientation']! as String))); + break; + default: + throw MissingPluginException(); + } + } + + /// Converts messages received from the native platform into camera events. + /// + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + Future handleCameraMethodCall(MethodCall call, int cameraId) async { + switch (call.method) { + case 'initialized': + cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + call.arguments['previewWidth']! as double, + call.arguments['previewHeight']! as double, + deserializeExposureMode(call.arguments['exposureMode']! as String), + call.arguments['exposurePointSupported']! as bool, + deserializeFocusMode(call.arguments['focusMode']! as String), + call.arguments['focusPointSupported']! as bool, + )); + break; + case 'resolution_changed': + cameraEventStreamController.add(CameraResolutionChangedEvent( + cameraId, + call.arguments['captureWidth']! as double, + call.arguments['captureHeight']! as double, + )); + break; + case 'camera_closing': + cameraEventStreamController.add(CameraClosingEvent( + cameraId, + )); + break; + case 'video_recorded': + cameraEventStreamController.add(VideoRecordedEvent( + cameraId, + XFile(call.arguments['path']! as String), + call.arguments['maxVideoDuration'] != null + ? Duration( + milliseconds: call.arguments['maxVideoDuration']! as int) + : null, + )); + break; + case 'error': + cameraEventStreamController.add(CameraErrorEvent( + cameraId, + call.arguments['description']! as String, + )); + break; + default: + throw MissingPluginException(); + } + } +} diff --git a/packages/camera/camera_android/lib/src/type_conversion.dart b/packages/camera/camera_android/lib/src/type_conversion.dart new file mode 100644 index 000000000000..754a5a032715 --- /dev/null +++ b/packages/camera/camera_android/lib/src/type_conversion.dart @@ -0,0 +1,49 @@ +// 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'; + +/// Converts method channel call [data] for `receivedImageStreamData` to a +/// [CameraImageData]. +CameraImageData cameraImageFromPlatformData(Map data) { + return CameraImageData( + format: _cameraImageFormatFromPlatformData(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) => _cameraImagePlaneFromPlatformData( + planeData as Map)))); +} + +CameraImageFormat _cameraImageFormatFromPlatformData(dynamic data) { + return CameraImageFormat(_imageFormatGroupFromPlatformData(data), raw: data); +} + +ImageFormatGroup _imageFormatGroupFromPlatformData(dynamic data) { + switch (data) { + case 35: // android.graphics.ImageFormat.YUV_420_888 + return ImageFormatGroup.yuv420; + case 256: // android.graphics.ImageFormat.JPEG + return ImageFormatGroup.jpeg; + } + + return ImageFormatGroup.unknown; +} + +CameraImagePlane _cameraImagePlaneFromPlatformData(Map data) { + return CameraImagePlane( + 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?); +} diff --git a/packages/camera/camera_android/lib/src/utils.dart b/packages/camera/camera_android/lib/src/utils.dart new file mode 100644 index 000000000000..663ec6da7a97 --- /dev/null +++ b/packages/camera/camera_android/lib/src/utils.dart @@ -0,0 +1,51 @@ +// 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_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; + +/// Parses a string into a corresponding CameraLensDirection. +CameraLensDirection parseCameraLensDirection(String string) { + switch (string) { + case 'front': + return CameraLensDirection.front; + case 'back': + return CameraLensDirection.back; + case 'external': + return CameraLensDirection.external; + } + throw ArgumentError('Unknown CameraLensDirection value'); +} + +/// Returns the device orientation as a String. +String serializeDeviceOrientation(DeviceOrientation orientation) { + switch (orientation) { + case DeviceOrientation.portraitUp: + return 'portraitUp'; + case DeviceOrientation.portraitDown: + return 'portraitDown'; + case DeviceOrientation.landscapeRight: + return 'landscapeRight'; + case DeviceOrientation.landscapeLeft: + return 'landscapeLeft'; + default: + throw ArgumentError('Unknown DeviceOrientation value'); + } +} + +/// Returns the device orientation for a given String. +DeviceOrientation deserializeDeviceOrientation(String str) { + switch (str) { + case 'portraitUp': + return DeviceOrientation.portraitUp; + case 'portraitDown': + return DeviceOrientation.portraitDown; + case 'landscapeRight': + return DeviceOrientation.landscapeRight; + case 'landscapeLeft': + return DeviceOrientation.landscapeLeft; + default: + throw ArgumentError('"$str" is not a valid DeviceOrientation value'); + } +} diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml new file mode 100644 index 000000000000..6f1b667670e8 --- /dev/null +++ b/packages/camera/camera_android/pubspec.yaml @@ -0,0 +1,32 @@ +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.0+4 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: camera + platforms: + android: + package: io.flutter.plugins.camera + pluginClass: CameraPlugin + dartPluginClass: AndroidCamera + +dependencies: + camera_platform_interface: ^2.2.0 + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.2 + stream_transform: ^2.0.0 + +dev_dependencies: + async: ^2.5.0 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter diff --git a/packages/camera/camera_android/test/android_camera_test.dart b/packages/camera/camera_android/test/android_camera_test.dart new file mode 100644 index 000000000000..3e50e6918648 --- /dev/null +++ b/packages/camera/camera_android/test/android_camera_test.dart @@ -0,0 +1,1094 @@ +// 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:math'; + +import 'package:async/async.dart'; +import 'package:camera_android/src/android_camera.dart'; +import 'package:camera_android/src/utils.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'method_channel_mock.dart'; + +const String _channelName = 'plugins.flutter.io/camera_android'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('registers instance', () async { + AndroidCamera.registerWith(); + expect(CameraPlatform.instance, isA()); + }); + + test('registration does not set message handlers', () async { + AndroidCamera.registerWith(); + + // Setting up a handler requires bindings to be initialized, and since + // 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) {}); + expect(response, null); + }); + + group('Creation, Initialization & Disposal Tests', () { + test('Should send creation data and receive back a camera id', () async { + // Arrange + final MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: _channelName, + methods: { + 'create': { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + } + }); + final AndroidCamera camera = AndroidCamera(); + + // Act + final int cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0), + ResolutionPreset.high, + ); + + // Assert + expect(cameraMockChannel.log, [ + isMethodCall( + 'create', + arguments: { + 'cameraName': 'Test', + 'resolutionPreset': 'high', + 'enableAudio': false + }, + ), + ]); + expect(cameraId, 1); + }); + + test('Should throw CameraException when create throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: _channelName, methods: { + 'create': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + final AndroidCamera camera = AndroidCamera(); + + // Act + expect( + () => camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ), + throwsA( + isA() + .having( + (CameraException e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((CameraException e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test('Should throw CameraException when create throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: _channelName, methods: { + 'create': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + final AndroidCamera camera = AndroidCamera(); + + // Act + expect( + () => camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ), + throwsA( + isA() + .having( + (CameraException e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((CameraException e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test( + 'Should throw CameraException when initialize throws a PlatformException', + () { + // Arrange + MethodChannelMock( + channelName: _channelName, + methods: { + 'initialize': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }, + ); + final AndroidCamera camera = AndroidCamera(); + + // Act + expect( + () => camera.initializeCamera(0), + throwsA( + isA() + .having( + (CameraException e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having( + (CameraException e) => e.description, + 'description', + 'Mock error message used during testing.', + ), + ), + ); + }, + ); + + test('Should send initialization data', () async { + // Arrange + final MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: _channelName, + methods: { + 'create': { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + }, + 'initialize': null + }); + final AndroidCamera camera = AndroidCamera(); + final int cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + + // Act + final Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + )); + await initializeFuture; + + // Assert + expect(cameraId, 1); + expect(cameraMockChannel.log, [ + anything, + isMethodCall( + 'initialize', + arguments: { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + }, + ), + ]); + }); + + test('Should send a disposal call on dispose', () async { + // Arrange + final MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: _channelName, + methods: { + 'create': {'cameraId': 1}, + 'initialize': null, + 'dispose': {'cameraId': 1} + }); + + final AndroidCamera camera = AndroidCamera(); + final int cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + final Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + )); + await initializeFuture; + + // Act + await camera.dispose(cameraId); + + // Assert + expect(cameraId, 1); + expect(cameraMockChannel.log, [ + anything, + anything, + isMethodCall( + 'dispose', + arguments: {'cameraId': 1}, + ), + ]); + }); + }); + + group('Event Tests', () { + late AndroidCamera camera; + late int cameraId; + setUp(() async { + MethodChannelMock( + channelName: _channelName, + methods: { + 'create': {'cameraId': 1}, + 'initialize': null + }, + ); + camera = AndroidCamera(); + cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + final Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + )); + await initializeFuture; + }); + + test('Should receive initialized event', () async { + // Act + final Stream eventStream = + camera.onCameraInitialized(cameraId); + final StreamQueue streamQueue = + StreamQueue(eventStream); + + // Emit test events + final CameraInitializedEvent event = CameraInitializedEvent( + cameraId, + 3840, + 2160, + ExposureMode.auto, + true, + FocusMode.auto, + true, + ); + await camera.handleCameraMethodCall( + MethodCall('initialized', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive resolution changes', () async { + // Act + final Stream resolutionStream = + camera.onCameraResolutionChanged(cameraId); + final StreamQueue streamQueue = + StreamQueue(resolutionStream); + + // Emit test events + final CameraResolutionChangedEvent fhdEvent = + CameraResolutionChangedEvent(cameraId, 1920, 1080); + final CameraResolutionChangedEvent uhdEvent = + CameraResolutionChangedEvent(cameraId, 3840, 2160); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, fhdEvent); + expect(await streamQueue.next, uhdEvent); + expect(await streamQueue.next, fhdEvent); + expect(await streamQueue.next, uhdEvent); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive camera closing events', () async { + // Act + final Stream eventStream = + camera.onCameraClosing(cameraId); + final StreamQueue streamQueue = + StreamQueue(eventStream); + + // Emit test events + final CameraClosingEvent event = CameraClosingEvent(cameraId); + await camera.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive camera error events', () async { + // Act + final Stream errorStream = + camera.onCameraError(cameraId); + final StreamQueue streamQueue = + StreamQueue(errorStream); + + // Emit test events + final CameraErrorEvent event = + CameraErrorEvent(cameraId, 'Error Description'); + await camera.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive device orientation change events', () async { + // Act + final Stream eventStream = + camera.onDeviceOrientationChanged(); + final StreamQueue streamQueue = + StreamQueue(eventStream); + + // Emit test events + const DeviceOrientationChangedEvent event = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + for (int i = 0; i < 3; i++) { + await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + .handlePlatformMessage( + AndroidCamera.deviceEventChannelName, + const StandardMethodCodec().encodeMethodCall( + MethodCall('orientation_changed', event.toJson())), + null); + } + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + }); + + group('Function Tests', () { + late AndroidCamera camera; + late int cameraId; + + setUp(() async { + MethodChannelMock( + channelName: _channelName, + methods: { + 'create': {'cameraId': 1}, + 'initialize': null + }, + ); + camera = AndroidCamera(); + cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + final Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add( + CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + ), + ); + await initializeFuture; + }); + + test('Should fetch CameraDescription instances for available cameras', + () async { + // Arrange + final List returnData = [ + { + 'name': 'Test 1', + 'lensFacing': 'front', + 'sensorOrientation': 1 + }, + { + 'name': 'Test 2', + 'lensFacing': 'back', + 'sensorOrientation': 2 + } + ]; + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'availableCameras': returnData}, + ); + + // Act + final List cameras = await camera.availableCameras(); + + // Assert + expect(channel.log, [ + isMethodCall('availableCameras', arguments: null), + ]); + expect(cameras.length, returnData.length); + for (int i = 0; i < returnData.length; i++) { + final CameraDescription cameraDescription = CameraDescription( + name: returnData[i]['name']! as String, + lensDirection: + parseCameraLensDirection(returnData[i]['lensFacing']! as String), + sensorOrientation: returnData[i]['sensorOrientation']! as int, + ); + expect(cameras[i], cameraDescription); + } + }); + + test( + 'Should throw CameraException when availableCameras throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: _channelName, methods: { + 'availableCameras': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + + // Act + expect( + camera.availableCameras, + throwsA( + isA() + .having( + (CameraException e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((CameraException e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test('Should take a picture and return an XFile instance', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'takePicture': '/test/path.jpg'}); + + // Act + final XFile file = await camera.takePicture(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('takePicture', arguments: { + 'cameraId': cameraId, + }), + ]); + expect(file.path, '/test/path.jpg'); + }); + + test('Should prepare for video recording', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'prepareForVideoRecording': null}, + ); + + // Act + await camera.prepareForVideoRecording(); + + // Assert + expect(channel.log, [ + isMethodCall('prepareForVideoRecording', arguments: null), + ]); + }); + + test('Should start recording a video', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'startVideoRecording': null}, + ); + + // Act + await camera.startVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('startVideoRecording', arguments: { + 'cameraId': cameraId, + 'maxVideoDuration': null, + }), + ]); + }); + + test('Should pass maxVideoDuration when starting recording a video', + () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'startVideoRecording': null}, + ); + + // Act + await camera.startVideoRecording( + cameraId, + maxVideoDuration: const Duration(seconds: 10), + ); + + // Assert + expect(channel.log, [ + isMethodCall('startVideoRecording', arguments: { + 'cameraId': cameraId, + 'maxVideoDuration': 10000 + }), + ]); + }); + + test('Should stop a video recording and return the file', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'stopVideoRecording': '/test/path.mp4'}, + ); + + // Act + final XFile file = await camera.stopVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('stopVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + expect(file.path, '/test/path.mp4'); + }); + + test('Should pause a video recording', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'pauseVideoRecording': null}, + ); + + // Act + await camera.pauseVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('pauseVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should resume a video recording', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'resumeVideoRecording': null}, + ); + + // Act + await camera.resumeVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('resumeVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the flash mode', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setFlashMode': null}, + ); + + // Act + await camera.setFlashMode(cameraId, FlashMode.torch); + await camera.setFlashMode(cameraId, FlashMode.always); + await camera.setFlashMode(cameraId, FlashMode.auto); + await camera.setFlashMode(cameraId, FlashMode.off); + + // Assert + expect(channel.log, [ + isMethodCall('setFlashMode', arguments: { + 'cameraId': cameraId, + 'mode': 'torch' + }), + isMethodCall('setFlashMode', arguments: { + 'cameraId': cameraId, + 'mode': 'always' + }), + isMethodCall('setFlashMode', + arguments: {'cameraId': cameraId, 'mode': 'auto'}), + isMethodCall('setFlashMode', + arguments: {'cameraId': cameraId, 'mode': 'off'}), + ]); + }); + + test('Should set the exposure mode', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setExposureMode': null}, + ); + + // Act + await camera.setExposureMode(cameraId, ExposureMode.auto); + await camera.setExposureMode(cameraId, ExposureMode.locked); + + // Assert + expect(channel.log, [ + isMethodCall('setExposureMode', + arguments: {'cameraId': cameraId, 'mode': 'auto'}), + isMethodCall('setExposureMode', arguments: { + 'cameraId': cameraId, + 'mode': 'locked' + }), + ]); + }); + + test('Should set the exposure point', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setExposurePoint': null}, + ); + + // Act + await camera.setExposurePoint(cameraId, const Point(0.5, 0.5)); + await camera.setExposurePoint(cameraId, null); + + // Assert + expect(channel.log, [ + isMethodCall('setExposurePoint', arguments: { + 'cameraId': cameraId, + 'x': 0.5, + 'y': 0.5, + 'reset': false + }), + isMethodCall('setExposurePoint', arguments: { + 'cameraId': cameraId, + 'x': null, + 'y': null, + 'reset': true + }), + ]); + }); + + test('Should get the min exposure offset', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'getMinExposureOffset': 2.0}, + ); + + // Act + final double minExposureOffset = + await camera.getMinExposureOffset(cameraId); + + // Assert + expect(minExposureOffset, 2.0); + expect(channel.log, [ + isMethodCall('getMinExposureOffset', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the max exposure offset', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'getMaxExposureOffset': 2.0}, + ); + + // Act + final double maxExposureOffset = + await camera.getMaxExposureOffset(cameraId); + + // Assert + expect(maxExposureOffset, 2.0); + expect(channel.log, [ + isMethodCall('getMaxExposureOffset', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the exposure offset step size', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'getExposureOffsetStepSize': 0.25}, + ); + + // Act + final double stepSize = await camera.getExposureOffsetStepSize(cameraId); + + // Assert + expect(stepSize, 0.25); + expect(channel.log, [ + isMethodCall('getExposureOffsetStepSize', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the exposure offset', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setExposureOffset': 0.6}, + ); + + // Act + final double actualOffset = await camera.setExposureOffset(cameraId, 0.5); + + // Assert + expect(actualOffset, 0.6); + expect(channel.log, [ + isMethodCall('setExposureOffset', arguments: { + 'cameraId': cameraId, + 'offset': 0.5, + }), + ]); + }); + + test('Should set the focus mode', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setFocusMode': null}, + ); + + // Act + await camera.setFocusMode(cameraId, FocusMode.auto); + await camera.setFocusMode(cameraId, FocusMode.locked); + + // Assert + expect(channel.log, [ + isMethodCall('setFocusMode', + arguments: {'cameraId': cameraId, 'mode': 'auto'}), + isMethodCall('setFocusMode', arguments: { + 'cameraId': cameraId, + 'mode': 'locked' + }), + ]); + }); + + test('Should set the exposure point', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setFocusPoint': null}, + ); + + // Act + await camera.setFocusPoint(cameraId, const Point(0.5, 0.5)); + await camera.setFocusPoint(cameraId, null); + + // Assert + expect(channel.log, [ + isMethodCall('setFocusPoint', arguments: { + 'cameraId': cameraId, + 'x': 0.5, + 'y': 0.5, + 'reset': false + }), + isMethodCall('setFocusPoint', arguments: { + 'cameraId': cameraId, + 'x': null, + 'y': null, + 'reset': true + }), + ]); + }); + + test('Should build a texture widget as preview widget', () async { + // Act + final Widget widget = camera.buildPreview(cameraId); + + // Act + expect(widget is Texture, isTrue); + expect((widget as Texture).textureId, cameraId); + }); + + test('Should throw MissingPluginException when handling unknown method', + () { + final AndroidCamera camera = AndroidCamera(); + + expect( + () => camera.handleCameraMethodCall( + const MethodCall('unknown_method'), 1), + throwsA(isA())); + }); + + test('Should get the max zoom level', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'getMaxZoomLevel': 10.0}, + ); + + // Act + final double maxZoomLevel = await camera.getMaxZoomLevel(cameraId); + + // Assert + expect(maxZoomLevel, 10.0); + expect(channel.log, [ + isMethodCall('getMaxZoomLevel', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the min zoom level', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'getMinZoomLevel': 1.0}, + ); + + // Act + final double maxZoomLevel = await camera.getMinZoomLevel(cameraId); + + // Assert + expect(maxZoomLevel, 1.0); + expect(channel.log, [ + isMethodCall('getMinZoomLevel', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the zoom level', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setZoomLevel': null}, + ); + + // Act + await camera.setZoomLevel(cameraId, 2.0); + + // Assert + expect(channel.log, [ + isMethodCall('setZoomLevel', + arguments: {'cameraId': cameraId, 'zoom': 2.0}), + ]); + }); + + test('Should throw CameraException when illegal zoom level is supplied', + () async { + // Arrange + MethodChannelMock( + channelName: _channelName, + methods: { + 'setZoomLevel': PlatformException( + code: 'ZOOM_ERROR', + message: 'Illegal zoom error', + ) + }, + ); + + // Act & assert + expect( + () => camera.setZoomLevel(cameraId, -1.0), + throwsA(isA() + .having((CameraException e) => e.code, 'code', 'ZOOM_ERROR') + .having((CameraException e) => e.description, 'description', + 'Illegal zoom error'))); + }); + + test('Should lock the capture orientation', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'lockCaptureOrientation': null}, + ); + + // Act + await camera.lockCaptureOrientation( + cameraId, DeviceOrientation.portraitUp); + + // Assert + expect(channel.log, [ + isMethodCall('lockCaptureOrientation', arguments: { + 'cameraId': cameraId, + 'orientation': 'portraitUp' + }), + ]); + }); + + test('Should unlock the capture orientation', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'unlockCaptureOrientation': null}, + ); + + // Act + await camera.unlockCaptureOrientation(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('unlockCaptureOrientation', + arguments: {'cameraId': cameraId}), + ]); + }); + + test('Should pause the camera preview', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'pausePreview': null}, + ); + + // Act + await camera.pausePreview(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('pausePreview', + arguments: {'cameraId': cameraId}), + ]); + }); + + test('Should resume the camera preview', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'resumePreview': null}, + ); + + // Act + await camera.resumePreview(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('resumePreview', + arguments: {'cameraId': cameraId}), + ]); + }); + + test('Should start streaming', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: { + 'startImageStream': null, + 'stopImageStream': null, + }, + ); + + // Act + final StreamSubscription subscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData imageData) {}); + + // Assert + expect(channel.log, [ + isMethodCall('startImageStream', arguments: null), + ]); + + subscription.cancel(); + }); + + test('Should stop streaming', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: { + 'startImageStream': null, + 'stopImageStream': null, + }, + ); + + // Act + final StreamSubscription subscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData imageData) {}); + subscription.cancel(); + + // Assert + expect(channel.log, [ + isMethodCall('startImageStream', arguments: null), + isMethodCall('stopImageStream', arguments: null), + ]); + }); + }); +} diff --git a/packages/camera/camera/test/utils/method_channel_mock.dart b/packages/camera/camera_android/test/method_channel_mock.dart similarity index 95% rename from packages/camera/camera/test/utils/method_channel_mock.dart rename to packages/camera/camera_android/test/method_channel_mock.dart index 7c8b4ca3d3f0..413c10633cc1 100644 --- a/packages/camera/camera/test/utils/method_channel_mock.dart +++ b/packages/camera/camera_android/test/method_channel_mock.dart @@ -28,7 +28,7 @@ class MethodChannelMock { } return Future.delayed(delay ?? Duration.zero, () { - final Object? result = methods[methodCall.method]; + final dynamic result = methods[methodCall.method]; if (result is Exception) { throw result; } diff --git a/packages/camera/camera_android/test/type_conversion_test.dart b/packages/camera/camera_android/test/type_conversion_test.dart new file mode 100644 index 000000000000..b07466df791f --- /dev/null +++ b/packages/camera/camera_android/test/type_conversion_test.dart @@ -0,0 +1,60 @@ +// 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_android/src/type_conversion.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('CameraImageData can be created', () { + final CameraImageData cameraImage = + cameraImageFromPlatformData({ + 'format': 1, + 'height': 1, + 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.height, 1); + expect(cameraImage.width, 4); + expect(cameraImage.format.group, ImageFormatGroup.unknown); + expect(cameraImage.planes.length, 1); + }); + + test('CameraImageData has ImageFormatGroup.yuv420', () { + final CameraImageData cameraImage = + cameraImageFromPlatformData({ + 'format': 35, + 'height': 1, + 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + }); +} diff --git a/packages/camera/camera_android/test/utils_test.dart b/packages/camera/camera_android/test/utils_test.dart new file mode 100644 index 000000000000..6f426bc90f6f --- /dev/null +++ b/packages/camera/camera_android/test/utils_test.dart @@ -0,0 +1,60 @@ +// 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/src/utils.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Utility methods', () { + test( + 'Should return CameraLensDirection when valid value is supplied when parsing camera lens direction', + () { + expect( + parseCameraLensDirection('back'), + CameraLensDirection.back, + ); + expect( + parseCameraLensDirection('front'), + CameraLensDirection.front, + ); + expect( + parseCameraLensDirection('external'), + CameraLensDirection.external, + ); + }); + + test( + 'Should throw ArgumentException when invalid value is supplied when parsing camera lens direction', + () { + expect( + () => parseCameraLensDirection('test'), + throwsA(isArgumentError), + ); + }); + + test('serializeDeviceOrientation() should serialize correctly', () { + expect(serializeDeviceOrientation(DeviceOrientation.portraitUp), + 'portraitUp'); + expect(serializeDeviceOrientation(DeviceOrientation.portraitDown), + 'portraitDown'); + expect(serializeDeviceOrientation(DeviceOrientation.landscapeRight), + 'landscapeRight'); + expect(serializeDeviceOrientation(DeviceOrientation.landscapeLeft), + 'landscapeLeft'); + }); + + test('deserializeDeviceOrientation() should deserialize correctly', () { + expect(deserializeDeviceOrientation('portraitUp'), + DeviceOrientation.portraitUp); + expect(deserializeDeviceOrientation('portraitDown'), + DeviceOrientation.portraitDown); + expect(deserializeDeviceOrientation('landscapeRight'), + DeviceOrientation.landscapeRight); + expect(deserializeDeviceOrientation('landscapeLeft'), + DeviceOrientation.landscapeLeft); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/.metadata b/packages/camera/camera_android_camerax/.metadata new file mode 100644 index 000000000000..1667b9356657 --- /dev/null +++ b/packages/camera/camera_android_camerax/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 6b04999e4aaa9dfafdcb5ca09e812df7379d9ee5 + channel: spellcheck_1_1 + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 6b04999e4aaa9dfafdcb5ca09e812df7379d9ee5 + base_revision: 6b04999e4aaa9dfafdcb5ca09e812df7379d9ee5 + - platform: android + create_revision: 6b04999e4aaa9dfafdcb5ca09e812df7379d9ee5 + base_revision: 6b04999e4aaa9dfafdcb5ca09e812df7379d9ee5 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/camera/camera_android_camerax/AUTHORS b/packages/camera/camera_android_camerax/AUTHORS new file mode 100644 index 000000000000..557dff97933b --- /dev/null +++ b/packages/camera/camera_android_camerax/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md new file mode 100644 index 000000000000..ce2fb9046c69 --- /dev/null +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -0,0 +1,6 @@ +## NEXT + +* Creates camera_android_camerax plugin for development. +* Adds CameraInfo class and removes unnecessary code from plugin. +* Adds CameraSelector class. +* Adds ProcessCameraProvider class. diff --git a/packages/camera/camera_android_camerax/LICENSE b/packages/camera/camera_android_camerax/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/camera/camera_android_camerax/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/camera/camera_android_camerax/README.md b/packages/camera/camera_android_camerax/README.md new file mode 100644 index 000000000000..06d837ac7214 --- /dev/null +++ b/packages/camera/camera_android_camerax/README.md @@ -0,0 +1,3 @@ +# camera_android_camerax + +An implementation of the camera plugin on Android using CameraX. diff --git a/packages/camera/camera_android_camerax/android/build.gradle b/packages/camera/camera_android_camerax/android/build.gradle new file mode 100644 index 000000000000..d20643f70268 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/build.gradle @@ -0,0 +1,68 @@ +group 'io.flutter.plugins.camerax' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + // CameraX dependencies require compilation against version 33 or later. + compileSdkVersion 33 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + // Many of the CameraX APIs require API 21. + minSdkVersion 21 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } + + lintOptions { + disable 'AndroidGradlePluginVersion' + disable 'GradleDependency' + } +} + +dependencies { + // CameraX core library using the camera2 implementation must use same version number. + def camerax_version = "1.2.0-beta02" + 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 'androidx.test:core:1.4.0' + testImplementation 'org.robolectric:robolectric:4.8' +} diff --git a/packages/camera/camera_android_camerax/android/settings.gradle b/packages/camera/camera_android_camerax/android/settings.gradle new file mode 100644 index 000000000000..613f994165a0 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'camera_android_camerax' diff --git a/packages/camera/camera_android_camerax/android/src/main/AndroidManifest.xml b/packages/camera/camera_android_camerax/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..ea4275c757cf --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + 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 new file mode 100644 index 000000000000..b8fbaf539c32 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java @@ -0,0 +1,97 @@ +// 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.content.Context; +import androidx.annotation.NonNull; +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; + +/** 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; + + /** + * Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment. + * + *

See {@code io.flutter.plugins.camera.MainActivity} for an example. + */ + public CameraAndroidCameraxPlugin() {} + + void setUp(BinaryMessenger binaryMessenger, Context context) { + // Set up instance manager. + instanceManager = + InstanceManager.open( + identifier -> { + new GeneratedCameraXLibrary.JavaObjectFlutterApi(binaryMessenger) + .dispose(identifier, reply -> {}); + }); + + // 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)); + processCameraProviderHostApi = + new ProcessCameraProviderHostApiImpl(binaryMessenger, instanceManager, context); + GeneratedCameraXLibrary.ProcessCameraProviderHostApi.setup( + binaryMessenger, processCameraProviderHostApi); + } + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { + pluginBinding = flutterPluginBinding; + (new CameraAndroidCameraxPlugin()) + .setUp( + flutterPluginBinding.getBinaryMessenger(), + flutterPluginBinding.getApplicationContext()); + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + if (instanceManager != null) { + instanceManager.close(); + } + } + + // Activity Lifecycle methods: + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) { + updateContext(activityPluginBinding.getActivity()); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + updateContext(pluginBinding.getApplicationContext()); + } + + @Override + public void onReattachedToActivityForConfigChanges( + @NonNull ActivityPluginBinding activityPluginBinding) { + updateContext(activityPluginBinding.getActivity()); + } + + @Override + public void onDetachedFromActivity() { + updateContext(pluginBinding.getApplicationContext()); + } + + /** + * Updates context that is used to fetch the corresponding instance of a {@code + * ProcessCameraProvider}. + */ + private 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/CameraInfoFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoFlutterApiImpl.java new file mode 100644 index 000000000000..c538e420cc7e --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoFlutterApiImpl.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.camera.core.CameraInfo; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraInfoFlutterApi; + +public class CameraInfoFlutterApiImpl extends CameraInfoFlutterApi { + private final InstanceManager instanceManager; + + public CameraInfoFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + void create(CameraInfo cameraInfo, Reply reply) { + create(instanceManager.addHostCreatedInstance(cameraInfo), 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 new file mode 100644 index 000000000000..7daba0d38d6a --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoHostApiImpl.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.camera.core.CameraInfo; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraInfoHostApi; + +public class CameraInfoHostApiImpl implements CameraInfoHostApi { + private final InstanceManager instanceManager; + + public CameraInfoHostApiImpl(InstanceManager instanceManager) { + this.instanceManager = instanceManager; + } + + @Override + public Long getSensorRotationDegrees(@NonNull Long identifier) { + CameraInfo cameraInfo = (CameraInfo) instanceManager.getInstance(identifier); + return Long.valueOf(cameraInfo.getSensorRotationDegrees()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorFlutterApiImpl.java new file mode 100644 index 000000000000..6ca3782d8b59 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorFlutterApiImpl.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.camera.core.CameraSelector; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraSelectorFlutterApi; + +public class CameraSelectorFlutterApiImpl extends CameraSelectorFlutterApi { + private final InstanceManager instanceManager; + + public CameraSelectorFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + void create(CameraSelector cameraSelector, Long lensFacing, Reply reply) { + create(instanceManager.addHostCreatedInstance(cameraSelector), lensFacing, reply); + } +} 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 new file mode 100644 index 000000000000..9c559a72e63c --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorHostApiImpl.java @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.camera.core.CameraInfo; +import androidx.camera.core.CameraSelector; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraSelectorHostApi; +import java.util.ArrayList; +import java.util.List; + +public class CameraSelectorHostApiImpl implements CameraSelectorHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + + @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy(); + + public CameraSelectorHostApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + } + + @Override + public void create(@NonNull Long identifier, Long lensFacing) { + CameraSelector.Builder cameraSelectorBuilder = cameraXProxy.createCameraSelectorBuilder(); + CameraSelector cameraSelector; + + if (lensFacing != null) { + cameraSelector = cameraSelectorBuilder.requireLensFacing(Math.toIntExact(lensFacing)).build(); + } else { + cameraSelector = cameraSelectorBuilder.build(); + } + + instanceManager.addDartCreatedInstance(cameraSelector, identifier); + } + + @Override + public List filter(@NonNull Long identifier, @NonNull List cameraInfoIds) { + CameraSelector cameraSelector = (CameraSelector) instanceManager.getInstance(identifier); + List cameraInfosForFilter = new ArrayList(); + + for (Number cameraInfoAsNumber : cameraInfoIds) { + Long cameraInfoId = cameraInfoAsNumber.longValue(); + + CameraInfo cameraInfo = (CameraInfo) 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); + } + + return filteredCameraInfosIds; + } +} 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 new file mode 100644 index 000000000000..8063866d2fc6 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.camera.core.CameraSelector; + +public class CameraXProxy { + public CameraSelector.Builder createCameraSelectorBuilder() { + return new CameraSelector.Builder(); + } +} 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 new file mode 100644 index 000000000000..041564c3bfcb --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java @@ -0,0 +1,457 @@ +// 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 + +package io.flutter.plugins.camerax; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) +public class GeneratedCameraXLibrary { + + public interface Result { + void success(T result); + + void error(Throwable error); + } + + private static class JavaObjectHostApiCodec extends StandardMessageCodec { + public static final JavaObjectHostApiCodec INSTANCE = new JavaObjectHostApiCodec(); + + private JavaObjectHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface JavaObjectHostApi { + void dispose(@NonNull Long identifier); + + /** The codec used by JavaObjectHostApi. */ + static MessageCodec getCodec() { + return JavaObjectHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `JavaObjectHostApi` to handle messages through the `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, JavaObjectHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.JavaObjectHostApi.dispose", 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.dispose((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); + } + } + } + } + + private static class JavaObjectFlutterApiCodec extends StandardMessageCodec { + public static final JavaObjectFlutterApiCodec INSTANCE = new JavaObjectFlutterApiCodec(); + + private JavaObjectFlutterApiCodec() {} + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class JavaObjectFlutterApi { + private final BinaryMessenger binaryMessenger; + + public JavaObjectFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return JavaObjectFlutterApiCodec.INSTANCE; + } + + public void dispose(@NonNull Long identifierArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.JavaObjectFlutterApi.dispose", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg)), + channelReply -> { + callback.reply(null); + }); + } + } + + private static class CameraInfoHostApiCodec extends StandardMessageCodec { + public static final CameraInfoHostApiCodec INSTANCE = new CameraInfoHostApiCodec(); + + private CameraInfoHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface CameraInfoHostApi { + @NonNull + Long getSensorRotationDegrees(@NonNull Long identifier); + + /** The codec used by CameraInfoHostApi. */ + static MessageCodec getCodec() { + return CameraInfoHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `CameraInfoHostApi` to handle messages through the `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, CameraInfoHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees", + 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.getSensorRotationDegrees( + (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 class CameraInfoFlutterApiCodec extends StandardMessageCodec { + public static final CameraInfoFlutterApiCodec INSTANCE = new CameraInfoFlutterApiCodec(); + + private CameraInfoFlutterApiCodec() {} + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class CameraInfoFlutterApi { + private final BinaryMessenger binaryMessenger; + + public CameraInfoFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return CameraInfoFlutterApiCodec.INSTANCE; + } + + public void create(@NonNull Long identifierArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CameraInfoFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg)), + channelReply -> { + callback.reply(null); + }); + } + } + + private static class CameraSelectorHostApiCodec extends StandardMessageCodec { + public static final CameraSelectorHostApiCodec INSTANCE = new CameraSelectorHostApiCodec(); + + private CameraSelectorHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface CameraSelectorHostApi { + void create(@NonNull Long identifier, @Nullable Long lensFacing); + + @NonNull + List filter(@NonNull Long identifier, @NonNull List cameraInfoIds); + + /** The codec used by CameraSelectorHostApi. */ + static MessageCodec getCodec() { + return CameraSelectorHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `CameraSelectorHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, CameraSelectorHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CameraSelectorHostApi.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 lensFacingArg = (Number) args.get(1); + api.create( + (identifierArg == null) ? null : identifierArg.longValue(), + (lensFacingArg == null) ? null : lensFacingArg.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.CameraSelectorHostApi.filter", 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 cameraInfoIdsArg = (List) args.get(1); + if (cameraInfoIdsArg == null) { + throw new NullPointerException("cameraInfoIdsArg unexpectedly null."); + } + List output = + api.filter( + (identifierArg == null) ? null : identifierArg.longValue(), + cameraInfoIdsArg); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class CameraSelectorFlutterApiCodec extends StandardMessageCodec { + public static final CameraSelectorFlutterApiCodec INSTANCE = + new CameraSelectorFlutterApiCodec(); + + private CameraSelectorFlutterApiCodec() {} + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class CameraSelectorFlutterApi { + private final BinaryMessenger binaryMessenger; + + public CameraSelectorFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return CameraSelectorFlutterApiCodec.INSTANCE; + } + + public void create( + @NonNull Long identifierArg, @Nullable Long lensFacingArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CameraSelectorFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg, lensFacingArg)), + channelReply -> { + callback.reply(null); + }); + } + } + + private static class ProcessCameraProviderHostApiCodec extends StandardMessageCodec { + public static final ProcessCameraProviderHostApiCodec INSTANCE = + new ProcessCameraProviderHostApiCodec(); + + private ProcessCameraProviderHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface ProcessCameraProviderHostApi { + void getInstance(Result result); + + @NonNull + List getAvailableCameraInfos(@NonNull Long identifier); + + /** The codec used by ProcessCameraProviderHostApi. */ + static MessageCodec getCodec() { + return ProcessCameraProviderHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `ProcessCameraProviderHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, ProcessCameraProviderHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ProcessCameraProviderHostApi.getInstance", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + Result resultCallback = + new Result() { + public void success(Long result) { + wrapped.put("result", result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.getInstance(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.ProcessCameraProviderHostApi.getAvailableCameraInfos", + 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 output = + api.getAvailableCameraInfos( + (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 class ProcessCameraProviderFlutterApiCodec extends StandardMessageCodec { + public static final ProcessCameraProviderFlutterApiCodec INSTANCE = + new ProcessCameraProviderFlutterApiCodec(); + + private ProcessCameraProviderFlutterApiCodec() {} + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class ProcessCameraProviderFlutterApi { + private final BinaryMessenger binaryMessenger; + + public ProcessCameraProviderFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return ProcessCameraProviderFlutterApiCodec.INSTANCE; + } + + public void create(@NonNull Long identifierArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg)), + channelReply -> { + callback.reply(null); + }); + } + } + + private static Map wrapError(Throwable exception) { + Map errorMap = new HashMap<>(); + errorMap.put("message", exception.toString()); + errorMap.put("code", exception.getClass().getSimpleName()); + errorMap.put( + "details", + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + return errorMap; + } +} 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 new file mode 100644 index 000000000000..9b549d7bd1ea --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java @@ -0,0 +1,210 @@ +// 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.os.Handler; +import android.os.Looper; +import androidx.annotation.Nullable; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.WeakHashMap; + +/** + * Maintains instances used to communicate with the corresponding objects in Dart. + * + *

When an instance is added with an identifier, either can be used to retrieve the other. + * + *

Added instances are added as a weak reference and a strong reference. When the strong + * reference is removed with `{@link #remove(long)}` and the weak reference is deallocated, the + * `finalizationListener` is made with the instance's identifier. However, if the strong reference + * is removed and then the identifier is retrieved with the intention to pass the identifier to Dart + * (e.g. calling {@link #getIdentifierForStrongReference(Object)}), the strong reference to the + * instance is recreated. The strong reference will then need to be removed manually again. + */ +@SuppressWarnings("unchecked") +public class InstanceManager { + // Identifiers are locked to a specific range to avoid collisions with objects + // created simultaneously from Dart. + // Host uses identifiers >= 2^16 and Dart is expected to use values n where, + // 0 <= n < 2^16. + private static final long MIN_HOST_CREATED_IDENTIFIER = 65536; + private static final long CLEAR_FINALIZED_WEAK_REFERENCES_INTERVAL = 30000; + + /** Interface for listening when a weak reference of an instance is removed from the manager. */ + public interface FinalizationListener { + void onFinalize(long identifier); + } + + private final WeakHashMap identifiers = new WeakHashMap<>(); + private final HashMap> weakInstances = new HashMap<>(); + private final HashMap strongInstances = new HashMap<>(); + + private final ReferenceQueue referenceQueue = new ReferenceQueue<>(); + private final HashMap, Long> weakReferencesToIdentifiers = new HashMap<>(); + + private final Handler handler = new Handler(Looper.getMainLooper()); + + private final FinalizationListener finalizationListener; + + private long nextIdentifier = MIN_HOST_CREATED_IDENTIFIER; + private boolean isClosed = false; + + /** + * Instantiate a new manager. + * + *

When the manager is no longer needed, {@link #close()} must be called. + * + * @param finalizationListener the listener for garbage collected weak references. + * @return a new `InstanceManager`. + */ + public static InstanceManager open(FinalizationListener finalizationListener) { + return new InstanceManager(finalizationListener); + } + + private InstanceManager(FinalizationListener finalizationListener) { + this.finalizationListener = finalizationListener; + handler.postDelayed( + this::releaseAllFinalizedInstances, CLEAR_FINALIZED_WEAK_REFERENCES_INTERVAL); + } + + /** + * Removes `identifier` and its associated strongly referenced instance, if present, from the + * manager. + * + * @param identifier the identifier paired to an instance. + * @param the expected return type. + * @return the removed instance if the manager contains the given identifier, otherwise null. + */ + @Nullable + public T remove(long identifier) { + assertManagerIsNotClosed(); + return (T) strongInstances.remove(identifier); + } + + /** + * Retrieves the identifier paired with an instance. + * + *

If the manager contains `instance`, as a strong or weak reference, the strong reference to + * `instance` will be recreated and will need to be removed again with {@link #remove(long)}. + * + * @param instance an instance that may be stored in the manager. + * @return the identifier associated with `instance` if the manager contains the value, otherwise + * null. + */ + @Nullable + public Long getIdentifierForStrongReference(Object instance) { + assertManagerIsNotClosed(); + final Long identifier = identifiers.get(instance); + if (identifier != null) { + strongInstances.put(identifier, instance); + } + return identifier; + } + + /** + * Adds a new instance that was instantiated from Dart. + * + *

If an instance or identifier has already been added, it will be replaced by the new values. + * The Dart InstanceManager is considered the source of truth and has the capability to overwrite + * stored pairs in response to hot restarts. + * + * @param instance the instance to be stored. + * @param identifier the identifier to be paired with instance. This value must be >= 0. + */ + public void addDartCreatedInstance(Object instance, long identifier) { + assertManagerIsNotClosed(); + addInstance(instance, 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. + * + * @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; + } + + /** + * Retrieves the instance associated with identifier. + * + * @param identifier the identifier paired to an instance. + * @param the expected return type. + * @return the instance associated with `identifier` if the manager contains the value, otherwise + * null. + */ + @Nullable + public T getInstance(long identifier) { + assertManagerIsNotClosed(); + final WeakReference instance = (WeakReference) weakInstances.get(identifier); + if (instance != null) { + return instance.get(); + } + return (T) strongInstances.get(identifier); + } + + /** + * Returns whether this manager contains the given `instance`. + * + * @param instance the instance whose presence in this manager is to be tested. + * @return whether this manager contains the given `instance`. + */ + public boolean containsInstance(Object instance) { + assertManagerIsNotClosed(); + return identifiers.containsKey(instance); + } + + /** + * Closes the manager and releases resources. + * + *

Calling a method after calling this one will throw an {@link AssertionError}. This method + * excluded. + */ + public void close() { + handler.removeCallbacks(this::releaseAllFinalizedInstances); + isClosed = true; + } + + private void releaseAllFinalizedInstances() { + WeakReference reference; + while ((reference = (WeakReference) referenceQueue.poll()) != null) { + final Long identifier = weakReferencesToIdentifiers.remove(reference); + if (identifier != null) { + weakInstances.remove(identifier); + strongInstances.remove(identifier); + finalizationListener.onFinalize(identifier); + } + } + handler.postDelayed( + this::releaseAllFinalizedInstances, CLEAR_FINALIZED_WEAK_REFERENCES_INTERVAL); + } + + private void addInstance(Object instance, long identifier) { + if (identifier < 0) { + throw new IllegalArgumentException("Identifier must be >= 0."); + } + final WeakReference weakReference = new WeakReference<>(instance, referenceQueue); + identifiers.put(instance, identifier); + weakInstances.put(identifier, weakReference); + weakReferencesToIdentifiers.put(weakReference, identifier); + strongInstances.put(identifier, instance); + } + + private void assertManagerIsNotClosed() { + if (isClosed) { + throw new AssertionError("Manager has already been closed."); + } + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/JavaObjectHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/JavaObjectHostApiImpl.java new file mode 100644 index 000000000000..5dc0ba7fc8ba --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/JavaObjectHostApiImpl.java @@ -0,0 +1,33 @@ +// 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.plugins.camerax.GeneratedCameraXLibrary.JavaObjectHostApi; + +/** + * A pigeon Host API implementation that handles creating {@link Object}s and invoking its static + * and instance methods. + * + *

{@link Object} instances created by {@link JavaObjectHostApiImpl} are used to intercommunicate + * with a paired Dart object. + */ +public class JavaObjectHostApiImpl implements JavaObjectHostApi { + private final InstanceManager instanceManager; + + /** + * Constructs a {@link JavaObjectHostApiImpl}. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + */ + public JavaObjectHostApiImpl(InstanceManager instanceManager) { + this.instanceManager = instanceManager; + } + + @Override + public void dispose(@NonNull Long identifier) { + instanceManager.remove(identifier); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderFlutterApiImpl.java new file mode 100644 index 000000000000..90c94d0c26cb --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderFlutterApiImpl.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.camera.lifecycle.ProcessCameraProvider; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ProcessCameraProviderFlutterApi; + +public class ProcessCameraProviderFlutterApiImpl extends ProcessCameraProviderFlutterApi { + public ProcessCameraProviderFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + private final InstanceManager instanceManager; + + void create(ProcessCameraProvider processCameraProvider, Reply reply) { + create(instanceManager.addHostCreatedInstance(processCameraProvider), reply); + } +} 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 new file mode 100644 index 000000000000..19c5eb5b3f70 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java @@ -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. + +package io.flutter.plugins.camerax; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.camera.core.CameraInfo; +import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.core.content.ContextCompat; +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; + +public class ProcessCameraProviderHostApiImpl implements ProcessCameraProviderHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + + private Context context; + + public ProcessCameraProviderHostApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager, Context context) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.context = context; + } + + /** + * Sets the context that the {@code ProcessCameraProvider} will use to attach the lifecycle of the + * camera to. + * + *

If using the camera plugin in an add-to-app context, ensure that a new instance of the + * {@code ProcessCameraProvider} is fetched via {@code #getInstance} anytime the context changes. + */ + public void setContext(Context context) { + this.context = context; + } + + /** + * Returns the instance of the ProcessCameraProvider to manage the lifecycle of the camera for the + * current {@code Context}. + */ + @Override + public void getInstance(GeneratedCameraXLibrary.Result result) { + ListenableFuture processCameraProviderFuture = + ProcessCameraProvider.getInstance(context); + + processCameraProviderFuture.addListener( + () -> { + try { + // Camera provider is now guaranteed to be available. + ProcessCameraProvider processCameraProvider = processCameraProviderFuture.get(); + + if (!instanceManager.containsInstance(processCameraProvider)) { + final ProcessCameraProviderFlutterApiImpl flutterApi = + new ProcessCameraProviderFlutterApiImpl(binaryMessenger, instanceManager); + flutterApi.create(processCameraProvider, reply -> {}); + } + result.success(instanceManager.getIdentifierForStrongReference(processCameraProvider)); + } catch (Exception e) { + result.error(e); + } + }, + ContextCompat.getMainExecutor(context)); + } + + /** Returns cameras available to the ProcessCameraProvider. */ + @Override + public List getAvailableCameraInfos(@NonNull Long identifier) { + ProcessCameraProvider processCameraProvider = + (ProcessCameraProvider) instanceManager.getInstance(identifier); + + List availableCameras = processCameraProvider.getAvailableCameraInfos(); + List availableCamerasIds = new ArrayList(); + final CameraInfoFlutterApiImpl cameraInfoFlutterApi = + new CameraInfoFlutterApiImpl(binaryMessenger, instanceManager); + + for (CameraInfo cameraInfo : availableCameras) { + cameraInfoFlutterApi.create(cameraInfo, result -> {}); + availableCamerasIds.add(instanceManager.getIdentifierForStrongReference(cameraInfo)); + } + return availableCamerasIds; + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraInfoTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraInfoTest.java new file mode 100644 index 000000000000..663d0e2f26d6 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraInfoTest.java @@ -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. + +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.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.camera.core.CameraInfo; +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 CameraInfoTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public CameraInfo mockCameraInfo; + @Mock public BinaryMessenger mockBinaryMessenger; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = InstanceManager.open(identifier -> {}); + } + + @After + public void tearDown() { + testInstanceManager.close(); + } + + @Test + public void getSensorRotationDegreesTest() { + final CameraInfoHostApiImpl cameraInfoHostApi = new CameraInfoHostApiImpl(testInstanceManager); + + testInstanceManager.addDartCreatedInstance(mockCameraInfo, 1); + + when(mockCameraInfo.getSensorRotationDegrees()).thenReturn(90); + + assertEquals((long) cameraInfoHostApi.getSensorRotationDegrees(1L), 90L); + verify(mockCameraInfo).getSensorRotationDegrees(); + } + + @Test + public void flutterApiCreateTest() { + final CameraInfoFlutterApiImpl spyFlutterApi = + spy(new CameraInfoFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + + spyFlutterApi.create(mockCameraInfo, reply -> {}); + + final long identifier = + Objects.requireNonNull(testInstanceManager.getIdentifierForStrongReference(mockCameraInfo)); + verify(spyFlutterApi).create(eq(identifier), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraSelectorTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraSelectorTest.java new file mode 100644 index 000000000000..2b27e08b5790 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraSelectorTest.java @@ -0,0 +1,97 @@ +// 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.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.camera.core.CameraInfo; +import androidx.camera.core.CameraSelector; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Arrays; +import java.util.List; +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 CameraSelectorTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public CameraSelector mockCameraSelector; + @Mock public BinaryMessenger mockBinaryMessenger; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = InstanceManager.open(identifier -> {}); + } + + @After + public void tearDown() { + testInstanceManager.close(); + } + + @Test + public void createTest() { + final CameraSelectorHostApiImpl cameraSelectorHostApi = + new CameraSelectorHostApiImpl(mockBinaryMessenger, testInstanceManager); + final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); + final CameraSelector.Builder mockCameraSelectorBuilder = mock(CameraSelector.Builder.class); + + cameraSelectorHostApi.cameraXProxy = mockCameraXProxy; + when(mockCameraXProxy.createCameraSelectorBuilder()).thenReturn(mockCameraSelectorBuilder); + + when(mockCameraSelectorBuilder.requireLensFacing(1)).thenReturn(mockCameraSelectorBuilder); + when(mockCameraSelectorBuilder.build()).thenReturn(mockCameraSelector); + + cameraSelectorHostApi.create(0L, 1L); + + verify(mockCameraSelectorBuilder).requireLensFacing(CameraSelector.LENS_FACING_BACK); + assertEquals(testInstanceManager.getInstance(0L), mockCameraSelector); + } + + @Test + public void filterTest() { + final CameraSelectorHostApiImpl cameraSelectorHostApi = + new CameraSelectorHostApiImpl(mockBinaryMessenger, testInstanceManager); + final CameraInfo cameraInfo = mock(CameraInfo.class); + final List cameraInfosForFilter = Arrays.asList(cameraInfo); + final List cameraInfosIds = Arrays.asList(1L); + + testInstanceManager.addDartCreatedInstance(mockCameraSelector, 0); + testInstanceManager.addDartCreatedInstance(cameraInfo, 1); + + when(mockCameraSelector.filter(cameraInfosForFilter)).thenReturn(cameraInfosForFilter); + + assertEquals( + cameraSelectorHostApi.filter(0L, cameraInfosIds), + Arrays.asList(testInstanceManager.getIdentifierForStrongReference(cameraInfo))); + verify(mockCameraSelector).filter(cameraInfosForFilter); + } + + @Test + public void flutterApiCreateTest() { + final CameraSelectorFlutterApiImpl spyFlutterApi = + spy(new CameraSelectorFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + + spyFlutterApi.create(mockCameraSelector, 0L, reply -> {}); + + final long identifier = + Objects.requireNonNull( + testInstanceManager.getIdentifierForStrongReference(mockCameraSelector)); + verify(spyFlutterApi).create(eq(identifier), eq(0L), any()); + } +} 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 new file mode 100644 index 000000000000..3878e05a40e8 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/InstanceManagerTest.java @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +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 org.junit.Test; + +public class InstanceManagerTest { + @Test + public void addDartCreatedInstance() { + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + + final Object object = new Object(); + instanceManager.addDartCreatedInstance(object, 0); + + assertEquals(object, instanceManager.getInstance(0)); + assertEquals((Long) 0L, instanceManager.getIdentifierForStrongReference(object)); + assertTrue(instanceManager.containsInstance(object)); + + instanceManager.close(); + } + + @Test + public void addHostCreatedInstance() { + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + + final Object object = new Object(); + long identifier = instanceManager.addHostCreatedInstance(object); + + assertNotNull(instanceManager.getInstance(identifier)); + assertEquals(object, instanceManager.getInstance(identifier)); + assertTrue(instanceManager.containsInstance(object)); + + instanceManager.close(); + } + + @Test + public void remove() { + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + + Object object = new Object(); + instanceManager.addDartCreatedInstance(object, 0); + + assertEquals(object, instanceManager.remove(0)); + + // To allow for object to be garbage collected. + //noinspection UnusedAssignment + object = null; + + Runtime.getRuntime().gc(); + + assertNull(instanceManager.getInstance(0)); + + instanceManager.close(); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/JavaObjectHostApiTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/JavaObjectHostApiTest.java new file mode 100644 index 000000000000..cce3341aaa89 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/JavaObjectHostApiTest.java @@ -0,0 +1,32 @@ +// 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.assertNull; + +import org.junit.Test; + +public class JavaObjectHostApiTest { + @Test + public void dispose() { + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + + final JavaObjectHostApiImpl hostApi = new JavaObjectHostApiImpl(instanceManager); + + Object object = new Object(); + instanceManager.addDartCreatedInstance(object, 0); + + // To free object for garbage collection. + //noinspection UnusedAssignment + object = null; + + hostApi.dispose(0L); + Runtime.getRuntime().gc(); + + assertNull(instanceManager.getInstance(0)); + + instanceManager.close(); + } +} 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 new file mode 100644 index 000000000000..5008e4ef34b0 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProcessCameraProviderTest.java @@ -0,0 +1,114 @@ +// 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.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import androidx.camera.core.CameraInfo; +import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.test.core.app.ApplicationProvider; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +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.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ProcessCameraProviderTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public ProcessCameraProvider processCameraProvider; + @Mock public BinaryMessenger mockBinaryMessenger; + + InstanceManager testInstanceManager; + private Context context; + + @Before + public void setUp() { + testInstanceManager = InstanceManager.open(identifier -> {}); + context = ApplicationProvider.getApplicationContext(); + } + + @After + public void tearDown() { + testInstanceManager.close(); + } + + @Test + public void getInstanceTest() { + final ProcessCameraProviderHostApiImpl processCameraProviderHostApi = + new ProcessCameraProviderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + final ListenableFuture processCameraProviderFuture = + spy(Futures.immediateFuture(processCameraProvider)); + final GeneratedCameraXLibrary.Result mockResult = + mock(GeneratedCameraXLibrary.Result.class); + + testInstanceManager.addDartCreatedInstance(processCameraProvider, 0); + + try (MockedStatic mockedProcessCameraProvider = + Mockito.mockStatic(ProcessCameraProvider.class)) { + mockedProcessCameraProvider + .when(() -> ProcessCameraProvider.getInstance(context)) + .thenAnswer( + (Answer>) + invocation -> processCameraProviderFuture); + + final ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); + + processCameraProviderHostApi.getInstance(mockResult); + verify(processCameraProviderFuture).addListener(runnableCaptor.capture(), any()); + runnableCaptor.getValue().run(); + verify(mockResult).success(0L); + } + } + + @Test + public void getAvailableCameraInfosTest() { + final ProcessCameraProviderHostApiImpl processCameraProviderHostApi = + new ProcessCameraProviderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + final CameraInfo mockCameraInfo = mock(CameraInfo.class); + + testInstanceManager.addDartCreatedInstance(processCameraProvider, 0); + testInstanceManager.addDartCreatedInstance(mockCameraInfo, 1); + + when(processCameraProvider.getAvailableCameraInfos()).thenReturn(Arrays.asList(mockCameraInfo)); + + assertEquals(processCameraProviderHostApi.getAvailableCameraInfos(0L), Arrays.asList(1L)); + verify(processCameraProvider).getAvailableCameraInfos(); + } + + @Test + public void flutterApiCreateTest() { + final ProcessCameraProviderFlutterApiImpl spyFlutterApi = + spy(new ProcessCameraProviderFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + + spyFlutterApi.create(processCameraProvider, reply -> {}); + + final long identifier = + Objects.requireNonNull( + testInstanceManager.getIdentifierForStrongReference(processCameraProvider)); + verify(spyFlutterApi).create(eq(identifier), any()); + } +} diff --git a/packages/camera/camera_android_camerax/example/README.md b/packages/camera/camera_android_camerax/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/camera/camera_android_camerax/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/camera/camera_android_camerax/example/android/app/build.gradle b/packages/camera/camera_android_camerax/example/android/app/build.gradle new file mode 100644 index 000000000000..0c0cbcd06921 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/app/build.gradle @@ -0,0 +1,66 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 33 + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "io.flutter.plugins.cameraxexample" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion 21 + targetSdkVersion 30 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.2.0' + api 'androidx.test:core:1.2.0' +} diff --git a/packages/camera/camera_android_camerax/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/camera/camera_android_camerax/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -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. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/camera/camera_android_camerax/example/android/app/src/androidTest/java/io/flutter/plugins/cameraxexample/MainActivityTest.java b/packages/camera/camera_android_camerax/example/android/app/src/androidTest/java/io/flutter/plugins/cameraxexample/MainActivityTest.java new file mode 100644 index 000000000000..8bcb398abb87 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/app/src/androidTest/java/io/flutter/plugins/cameraxexample/MainActivityTest.java @@ -0,0 +1,17 @@ +// 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.cameraxexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class MainActivityTest { + @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); +} diff --git a/packages/camera/camera_android_camerax/example/android/app/src/debug/AndroidManifest.xml b/packages/camera/camera_android_camerax/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..093e904635f7 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/camera/camera_android_camerax/example/android/app/src/main/AndroidManifest.xml b/packages/camera/camera_android_camerax/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..82b92e25bdfe --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/packages/camera/camera_android_camerax/example/android/app/src/main/java/io/flutter/plugins/cameraexample/MainActivity.java b/packages/camera/camera_android_camerax/example/android/app/src/main/java/io/flutter/plugins/cameraexample/MainActivity.java new file mode 100644 index 000000000000..5e2a10f1555a --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/app/src/main/java/io/flutter/plugins/cameraexample/MainActivity.java @@ -0,0 +1,9 @@ +// 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.cameraxexample; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity {} diff --git a/packages/camera/camera_android_camerax/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/camera/camera_android_camerax/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000000..f74085f3f6a2 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/camera/camera_android_camerax/example/android/app/src/main/res/drawable/launch_background.xml b/packages/camera/camera_android_camerax/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/camera/camera_android_camerax/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/camera/camera_android_camerax/example/android/app/src/main/res/values-night/styles.xml b/packages/camera/camera_android_camerax/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000000..06952be745f9 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/camera/camera_android_camerax/example/android/app/src/main/res/values/styles.xml b/packages/camera/camera_android_camerax/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..cb1ef88056ed --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/camera/camera_android_camerax/example/android/app/src/profile/AndroidManifest.xml b/packages/camera/camera_android_camerax/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000000..093e904635f7 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/camera/camera_android_camerax/example/android/build.gradle b/packages/camera/camera_android_camerax/example/android/build.gradle new file mode 100644 index 000000000000..20411f5f31a9 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.2.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/camera/camera_android_camerax/example/android/gradle.properties b/packages/camera/camera_android_camerax/example/android/gradle.properties new file mode 100644 index 000000000000..94adc3a3f97a --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/shared_preferences/shared_preferences_android/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera_android_camerax/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 93% rename from packages/shared_preferences/shared_preferences_android/android/gradle/wrapper/gradle-wrapper.properties rename to packages/camera/camera_android_camerax/example/android/gradle/wrapper/gradle-wrapper.properties index 3c9d0852bfa5..3c472b99c6f3 100644 --- a/packages/shared_preferences/shared_preferences_android/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/camera/camera_android_camerax/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/packages/camera/camera_android_camerax/example/android/settings.gradle b/packages/camera/camera_android_camerax/example/android/settings.gradle new file mode 100644 index 000000000000..44e62bcf06ae --- /dev/null +++ b/packages/camera/camera_android_camerax/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart b/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart new file mode 100644 index 000000000000..2b82b4bda5e4 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart @@ -0,0 +1,12 @@ +// 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:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('placeholder test', (WidgetTester tester) async {}); +} diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart new file mode 100644 index 000000000000..244a15281e3f --- /dev/null +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -0,0 +1,44 @@ +// 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_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/material.dart'; + +late List _cameras; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + _cameras = await CameraPlatform.instance.availableCameras(); + + runApp(const MyApp()); +} + +/// Example app +class MyApp extends StatefulWidget { + /// App instantiation + const MyApp({super.key}); + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + String availableCameraNames = 'Available cameras:'; + for (final CameraDescription cameraDescription in _cameras) { + availableCameraNames = '$availableCameraNames ${cameraDescription.name},'; + } + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Camera Example'), + ), + body: Center( + child: Text(availableCameraNames.substring( + 0, availableCameraNames.length - 1)), + ), + ), + ); + } +} diff --git a/packages/camera/camera_android_camerax/example/pubspec.yaml b/packages/camera/camera_android_camerax/example/pubspec.yaml new file mode 100644 index 000000000000..d9756f7ebd9b --- /dev/null +++ b/packages/camera/camera_android_camerax/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: camera_android_camerax_example +description: Demonstrates how to use the camera_android_camerax plugin. +publish_to: 'none' + +environment: + sdk: '>=2.17.0 <3.0.0' + flutter: ">=3.0.0" + +dependencies: + camera_android_camerax: + # When depending on this package from a real application you should use: + # camera_android_camerax: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + camera_platform_interface: ^2.2.0 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/camera/camera_android_camerax/example/test/widget_test.dart b/packages/camera/camera_android_camerax/example/test/widget_test.dart new file mode 100644 index 000000000000..bfe91af3eae6 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/test/widget_test.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. + +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Fake test', (WidgetTester tester) async { + expect(true, isTrue); + }); +} diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/test_driver/test/integration_test.dart b/packages/camera/camera_android_camerax/example/test_driver/integration_test.dart similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/example/test_driver/test/integration_test.dart rename to packages/camera/camera_android_camerax/example/test_driver/integration_test.dart diff --git a/packages/camera/camera_android_camerax/lib/camera_android_camerax.dart b/packages/camera/camera_android_camerax/lib/camera_android_camerax.dart new file mode 100644 index 000000000000..4ddecd71397b --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/camera_android_camerax.dart @@ -0,0 +1,5 @@ +// 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. + +export 'src/android_camera_camerax.dart'; 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 new file mode 100644 index 000000000000..f03273861793 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -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 'dart:async'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; + +/// The Android implementation of [CameraPlatform] that uses the CameraX library. +class AndroidCameraCameraX extends CameraPlatform { + /// Registers this class as the default instance of [CameraPlatform]. + static void registerWith() { + CameraPlatform.instance = AndroidCameraCameraX(); + } + + /// Returns list of all available cameras and their descriptions. + @override + Future> availableCameras() async { + throw UnimplementedError('availableCameras() is not implemented.'); + } +} 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 new file mode 100644 index 000000000000..9c6564a06c08 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart @@ -0,0 +1,61 @@ +// 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 'camera_info.dart'; +import 'camera_selector.dart'; +import 'camerax_library.pigeon.dart'; +import 'java_object.dart'; +import 'process_camera_provider.dart'; + +/// Handles initialization of Flutter APIs for the Android CameraX library. +class AndroidCameraXCameraFlutterApis { + /// Creates a [AndroidCameraXCameraFlutterApis]. + AndroidCameraXCameraFlutterApis({ + JavaObjectFlutterApiImpl? javaObjectFlutterApi, + CameraInfoFlutterApiImpl? cameraInfoFlutterApi, + CameraSelectorFlutterApiImpl? cameraSelectorFlutterApi, + ProcessCameraProviderFlutterApiImpl? processCameraProviderFlutterApi, + }) { + this.javaObjectFlutterApi = + javaObjectFlutterApi ?? JavaObjectFlutterApiImpl(); + this.cameraInfoFlutterApi = + cameraInfoFlutterApi ?? CameraInfoFlutterApiImpl(); + this.cameraSelectorFlutterApi = + cameraSelectorFlutterApi ?? CameraSelectorFlutterApiImpl(); + this.processCameraProviderFlutterApi = processCameraProviderFlutterApi ?? + ProcessCameraProviderFlutterApiImpl(); + } + + static bool _haveBeenSetUp = false; + + /// Mutable instance containing all Flutter Apis for Android CameraX Camera. + /// + /// This should only be changed for testing purposes. + static AndroidCameraXCameraFlutterApis instance = + AndroidCameraXCameraFlutterApis(); + + /// Handles callbacks methods for the native Java Object class. + late final JavaObjectFlutterApi javaObjectFlutterApi; + + /// Flutter Api for [CameraInfo]. + late final CameraInfoFlutterApiImpl cameraInfoFlutterApi; + + /// Flutter Api for [CameraSelector]. + late final CameraSelectorFlutterApiImpl cameraSelectorFlutterApi; + + /// Flutter Api for [ProcessCameraProvider]. + late final ProcessCameraProviderFlutterApiImpl + processCameraProviderFlutterApi; + + /// Ensures all the Flutter APIs have been setup to receive calls from native code. + void ensureSetUp() { + if (!_haveBeenSetUp) { + JavaObjectFlutterApi.setup(javaObjectFlutterApi); + CameraInfoFlutterApi.setup(cameraInfoFlutterApi); + CameraSelectorFlutterApi.setup(cameraSelectorFlutterApi); + ProcessCameraProviderFlutterApi.setup(processCameraProviderFlutterApi); + _haveBeenSetUp = true; + } + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/camera_info.dart b/packages/camera/camera_android_camerax/lib/src/camera_info.dart new file mode 100644 index 000000000000..d03426f40027 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/camera_info.dart @@ -0,0 +1,84 @@ +// 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.pigeon.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// Represents the metadata of a camera. +/// +/// See https://developer.android.com/reference/androidx/camera/core/CameraInfo. +class CameraInfo extends JavaObject { + /// Constructs a [CameraInfo] that is not automatically attached to a native object. + CameraInfo.detached( + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = CameraInfoHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + late final CameraInfoHostApiImpl _api; + + /// Gets sensor orientation degrees of camera. + Future getSensorRotationDegrees() => + _api.getSensorRotationDegreesFromInstance(this); +} + +/// Host API implementation of [CameraInfo]. +class CameraInfoHostApiImpl extends CameraInfoHostApi { + /// Constructs a [CameraInfoHostApiImpl]. + CameraInfoHostApiImpl( + {super.binaryMessenger, InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + } + + /// Maintains instances stored to communicate with native language objects. + late final InstanceManager instanceManager; + + /// Gets sensor orientation degrees of [CameraInfo]. + Future getSensorRotationDegreesFromInstance( + CameraInfo instance, + ) async { + final int sensorRotationDegrees = await getSensorRotationDegrees( + instanceManager.getIdentifier(instance)!); + return sensorRotationDegrees; + } +} + +/// Flutter API implementation of [CameraInfo]. +class CameraInfoFlutterApiImpl extends CameraInfoFlutterApi { + /// Constructs a [CameraInfoFlutterApiImpl]. + CameraInfoFlutterApiImpl({ + 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( + CameraInfo.detached( + binaryMessenger: binaryMessenger, instanceManager: instanceManager), + identifier, + onCopy: (CameraInfo original) { + return CameraInfo.detached( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + }, + ); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/camera_selector.dart b/packages/camera/camera_android_camerax/lib/src/camera_selector.dart new file mode 100644 index 000000000000..dd08b9bc571d --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/camera_selector.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. + +import 'package:flutter/services.dart'; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camera_info.dart'; +import 'camerax_library.pigeon.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// Selects a camera for use. +/// +/// See https://developer.android.com/reference/androidx/camera/core/CameraSelector. +class CameraSelector extends JavaObject { + /// Creates a [CameraSelector]. + CameraSelector( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + this.lensFacing}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = CameraSelectorHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + _api.createFromInstance(this, lensFacing); + } + + /// Creates a detached [CameraSelector]. + CameraSelector.detached( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + this.lensFacing}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = CameraSelectorHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + late final CameraSelectorHostApiImpl _api; + + /// ID for front facing lens. + static const int LENS_FACING_FRONT = 0; + + /// ID for back facing lens. + static const int LENS_FACING_BACK = 1; + + /// Selector for default front facing camera. + static CameraSelector getDefaultFrontCamera({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) { + return CameraSelector( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + lensFacing: LENS_FACING_FRONT, + ); + } + + /// Selector for default back facing camera. + static CameraSelector getDefaultBackCamera({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) { + return CameraSelector( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + lensFacing: LENS_FACING_BACK, + ); + } + + /// Lens direction of this selector. + final int? lensFacing; + + /// Filters available cameras based on provided [CameraInfo]s. + Future> filter(List cameraInfos) { + return _api.filterFromInstance(this, cameraInfos); + } +} + +/// Host API implementation of [CameraSelector]. +class CameraSelectorHostApiImpl extends CameraSelectorHostApi { + /// Constructs a [CameraSelectorHostApiImpl]. + CameraSelectorHostApiImpl( + {this.binaryMessenger, InstanceManager? instanceManager}) + : super(binaryMessenger: binaryMessenger) { + 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 [CameraSelector] with the lens direction provided if specified. + void createFromInstance(CameraSelector instance, int? lensFacing) { + int? identifier = instanceManager.getIdentifier(instance); + identifier ??= instanceManager.addDartCreatedInstance(instance, + onCopy: (CameraSelector original) { + return CameraSelector.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + lensFacing: original.lensFacing); + }); + + create(identifier, lensFacing); + } + + /// Filters a list of [CameraInfo]s based on the [CameraSelector]. + Future> filterFromInstance( + CameraSelector instance, + List cameraInfos, + ) async { + int? identifier = instanceManager.getIdentifier(instance); + identifier ??= instanceManager.addDartCreatedInstance(instance, + onCopy: (CameraSelector original) { + return CameraSelector.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + lensFacing: original.lensFacing); + }); + + final List cameraInfoIds = (cameraInfos.map( + (CameraInfo info) => instanceManager.getIdentifier(info)!)).toList(); + final List filteredCameraInfoIds = + await filter(identifier, cameraInfoIds); + if (filteredCameraInfoIds.isEmpty) { + return []; + } + return (filteredCameraInfoIds.map((int? id) => + instanceManager.getInstanceWithWeakReference(id!)! as CameraInfo)) + .toList(); + } +} + +/// Flutter API implementation of [CameraSelector]. +class CameraSelectorFlutterApiImpl implements CameraSelectorFlutterApi { + /// Constructs a [CameraSelectorFlutterApiImpl]. + CameraSelectorFlutterApiImpl({ + 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, int? lensFacing) { + instanceManager.addHostCreatedInstance( + CameraSelector.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + lensFacing: lensFacing), + identifier, + onCopy: (CameraSelector original) { + return CameraSelector.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + lensFacing: original.lensFacing); + }, + ); + } +} 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 new file mode 100644 index 000000000000..c0b052378def --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.pigeon.dart @@ -0,0 +1,374 @@ +// 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/instance_manager.dart b/packages/camera/camera_android_camerax/lib/src/instance_manager.dart new file mode 100644 index 000000000000..dd48610c8b56 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/instance_manager.dart @@ -0,0 +1,199 @@ +// 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'; + +/// Maintains instances used to communicate with the native objects they +/// represent. +/// +/// Added instances are stored as weak references and their copies are stored +/// as strong references to maintain access to their variables and callback +/// methods. Both are stored with the same identifier. +/// +/// When a weak referenced instance becomes inaccessible, +/// [onWeakReferenceRemoved] is called with its associated identifier. +/// +/// If an instance is retrieved and has the possibility to be used, +/// (e.g. calling [getInstanceWithWeakReference]) a copy of the strong reference +/// is added as a weak reference with the same identifier. This prevents a +/// scenario where the weak referenced instance was released and then later +/// returned by the host platform. +class InstanceManager { + /// Constructs an [InstanceManager]. + InstanceManager({required void Function(int) onWeakReferenceRemoved}) { + this.onWeakReferenceRemoved = (int identifier) { + debugPrint('Releasing weak reference with identifier: $identifier'); + _weakInstances.remove(identifier); + onWeakReferenceRemoved(identifier); + }; + _finalizer = Finalizer(this.onWeakReferenceRemoved); + } + + // Identifiers are locked to a specific range to avoid collisions with objects + // created simultaneously by the host platform. + // Host uses identifiers >= 2^16 and Dart is expected to use values n where, + // 0 <= n < 2^16. + static const int _maxDartCreatedIdentifier = 65536; + + // Expando is used because it doesn't prevent its keys from becoming + // inaccessible. This allows the manager to efficiently retrieve an identifier + // of an instance without holding a strong reference to that instance. + // + // It also doesn't use `==` to search for identifiers, which would lead to an + // infinite loop when comparing an object to its copy. (i.e. which was caused + // by calling instanceManager.getIdentifier() inside of `==` while this was a + // HashMap). + final Expando _identifiers = Expando(); + final Map> _weakInstances = + >{}; + final Map _strongInstances = {}; + final Map _copyCallbacks = {}; + late final Finalizer _finalizer; + int _nextIdentifier = 0; + + /// Called when a weak referenced instance is removed by [removeWeakReference] + /// or becomes inaccessible. + late final void Function(int) onWeakReferenceRemoved; + + /// Adds a new instance that was instantiated by Dart. + /// + /// In other words, Dart wants to add a new instance that will represent + /// an object that will be instantiated on the host platform. + /// + /// Throws assertion error if the instance has already been added. + /// + /// Returns the randomly generated id of the [instance] added. + int addDartCreatedInstance( + T instance, { + required T Function(T original) onCopy, + }) { + assert(getIdentifier(instance) == null); + + final int identifier = _nextUniqueIdentifier(); + _addInstanceWithIdentifier(instance, identifier, onCopy: onCopy); + return identifier; + } + + /// Removes the instance, if present, and call [onWeakReferenceRemoved] with + /// its identifier. + /// + /// Returns the identifier associated with the removed instance. Otherwise, + /// `null` if the instance was not found in this manager. + /// + /// This does not remove the the strong referenced instance associated with + /// [instance]. This can be done with [remove]. + int? removeWeakReference(Object instance) { + final int? identifier = getIdentifier(instance); + if (identifier == null) { + return null; + } + + _identifiers[instance] = null; + _finalizer.detach(instance); + onWeakReferenceRemoved(identifier); + + return identifier; + } + + /// Removes [identifier] and its associated strongly referenced instance, if + /// present, from the manager. + /// + /// Returns the strong referenced instance associated with [identifier] before + /// it was removed. Returns `null` if [identifier] was not associated with + /// any strong reference. + /// + /// This does not remove the the weak referenced instance associtated with + /// [identifier]. This can be done with [removeWeakReference]. + T? remove(int identifier) { + debugPrint('Releasing strong reference with identifier: $identifier'); + _copyCallbacks.remove(identifier); + return _strongInstances.remove(identifier) as T?; + } + + /// Retrieves the instance associated with identifier. + /// + /// The value returned is chosen from the following order: + /// + /// 1. A weakly referenced instance associated with identifier. + /// 2. If the only instance associated with identifier is a strongly + /// referenced instance, a copy of the instance is added as a weak reference + /// with the same identifier. Returning the newly created copy. + /// 3. If no instance is associated with identifier, returns null. + /// + /// This method also expects the host `InstanceManager` to have a strong + /// reference to the instance the identifier is associated with. + T? getInstanceWithWeakReference(int identifier) { + final Object? weakInstance = _weakInstances[identifier]?.target; + + if (weakInstance == null) { + final Object? strongInstance = _strongInstances[identifier]; + if (strongInstance != null) { + final Object copy = + _copyCallbacks[identifier]!(strongInstance)! as Object; + _identifiers[copy] = identifier; + _weakInstances[identifier] = WeakReference(copy); + _finalizer.attach(copy, identifier, detach: copy); + return copy as T; + } + return strongInstance as T?; + } + + return weakInstance as T; + } + + /// Retrieves the identifier associated with instance. + int? getIdentifier(Object instance) { + return _identifiers[instance]; + } + + /// Adds a new instance that was instantiated by the host platform. + /// + /// In other words, the host platform wants to add a new instance that + /// represents an object on the host platform. Stored with [identifier]. + /// + /// Throws assertion error if the instance or its identifier has already been + /// added. + /// + /// Returns unique identifier of the [instance] added. + void addHostCreatedInstance( + T instance, + int identifier, { + required T Function(T original) onCopy, + }) { + assert(!containsIdentifier(identifier)); + assert(getIdentifier(instance) == null); + assert(identifier >= 0); + _addInstanceWithIdentifier(instance, identifier, onCopy: onCopy); + } + + void _addInstanceWithIdentifier( + T instance, + int identifier, { + required T Function(T original) onCopy, + }) { + _identifiers[instance] = identifier; + _weakInstances[identifier] = WeakReference(instance); + _finalizer.attach(instance, identifier, detach: instance); + + final Object copy = onCopy(instance); + _identifiers[copy] = identifier; + _strongInstances[identifier] = copy; + _copyCallbacks[identifier] = onCopy; + } + + /// Whether this manager contains the given [identifier]. + bool containsIdentifier(int identifier) { + return _weakInstances.containsKey(identifier) || + _strongInstances.containsKey(identifier); + } + + int _nextUniqueIdentifier() { + late int identifier; + do { + identifier = _nextIdentifier; + _nextIdentifier = (_nextIdentifier + 1) % _maxDartCreatedIdentifier; + } while (containsIdentifier(identifier)); + return identifier; + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/java_object.dart b/packages/camera/camera_android_camerax/lib/src/java_object.dart new file mode 100644 index 000000000000..36a29ed0517b --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/java_object.dart @@ -0,0 +1,76 @@ +// 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' show immutable; +import 'package:flutter/services.dart'; + +import 'camerax_library.pigeon.dart'; +import 'instance_manager.dart'; + +/// Root of the Java class hierarchy. +/// +/// See https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html. +@immutable +class JavaObject { + /// Constructs a [JavaObject] without creating the associated Java object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + JavaObject.detached({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _api = JavaObjectHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + + /// Global instance of [InstanceManager]. + static final InstanceManager globalInstanceManager = InstanceManager( + onWeakReferenceRemoved: (int identifier) { + JavaObjectHostApiImpl().dispose(identifier); + }, + ); + + /// Pigeon Host Api implementation for [JavaObject]. + final JavaObjectHostApiImpl _api; + + /// Release the reference to a native Java instance. + static void dispose(JavaObject instance) { + instance._api.instanceManager.removeWeakReference(instance); + } +} + +/// Handles methods calls to the native Java Object class. +class JavaObjectHostApiImpl extends JavaObjectHostApi { + /// Constructs a [JavaObjectHostApiImpl]. + JavaObjectHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + 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; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; +} + +/// Handles callbacks methods for the native Java Object class. +class JavaObjectFlutterApiImpl implements JavaObjectFlutterApi { + /// Constructs a [JavaObjectFlutterApiImpl]. + JavaObjectFlutterApiImpl({InstanceManager? instanceManager}) + : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void dispose(int identifier) { + instanceManager.remove(identifier); + } +} 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 new file mode 100644 index 000000000000..e2b588d15faa --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart @@ -0,0 +1,119 @@ +// 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'; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camera_info.dart'; +import 'camerax_library.pigeon.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// Provides an object to manage the camera. +/// +/// See https://developer.android.com/reference/androidx/camera/lifecycle/ProcessCameraProvider. +class ProcessCameraProvider extends JavaObject { + /// Creates a detached [ProcessCameraProvider]. + ProcessCameraProvider.detached( + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = ProcessCameraProviderHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + late final ProcessCameraProviderHostApiImpl _api; + + /// Gets an instance of [ProcessCameraProvider]. + static Future getInstance( + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + final ProcessCameraProviderHostApiImpl api = + ProcessCameraProviderHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + + return api.getInstancefromInstances(); + } + + /// Retrieves the cameras available to the device. + Future> getAvailableCameraInfos() { + return _api.getAvailableCameraInfosFromInstances(this); + } +} + +/// Host API implementation of [ProcessCameraProvider]. +class ProcessCameraProviderHostApiImpl extends ProcessCameraProviderHostApi { + /// Creates a [ProcessCameraProviderHostApiImpl]. + ProcessCameraProviderHostApiImpl( + {this.binaryMessenger, InstanceManager? instanceManager}) + : super(binaryMessenger: binaryMessenger) { + 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; + + /// Retrieves an instance of a ProcessCameraProvider from the context of + /// the FlutterActivity. + Future getInstancefromInstances() async { + return instanceManager.getInstanceWithWeakReference(await getInstance())! + as ProcessCameraProvider; + } + + /// 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 List cameraInfos = await getAvailableCameraInfos(identifier); + return (cameraInfos.map((int? id) => + instanceManager.getInstanceWithWeakReference(id!)! as CameraInfo)) + .toList(); + } +} + +/// Flutter API Implementation of [ProcessCameraProvider]. +class ProcessCameraProviderFlutterApiImpl + implements ProcessCameraProviderFlutterApi { + /// Constructs a [ProcessCameraProviderFlutterApiImpl]. + ProcessCameraProviderFlutterApiImpl({ + 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( + ProcessCameraProvider.detached( + binaryMessenger: binaryMessenger, instanceManager: instanceManager), + identifier, + onCopy: (ProcessCameraProvider original) { + return ProcessCameraProvider.detached( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + }, + ); + } +} diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart new file mode 100644 index 000000000000..4d7d96910246 --- /dev/null +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -0,0 +1,72 @@ +// 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/camerax_library.pigeon.dart', + dartTestOut: 'test/test_camerax_library.pigeon.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', + 'found in the LICENSE file.', + ]), + javaOut: + 'android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java', + javaOptions: JavaOptions( + package: 'io.flutter.plugins.camerax', + className: 'GeneratedCameraXLibrary', + copyrightHeader: [ + '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.', + ], + ), + ), +) +@HostApi(dartHostTestHandler: 'TestJavaObjectHostApi') +abstract class JavaObjectHostApi { + void dispose(int identifier); +} + +@FlutterApi() +abstract class JavaObjectFlutterApi { + void dispose(int identifier); +} + +@HostApi(dartHostTestHandler: 'TestCameraInfoHostApi') +abstract class CameraInfoHostApi { + int getSensorRotationDegrees(int identifier); +} + +@FlutterApi() +abstract class CameraInfoFlutterApi { + void create(int identifier); +} + +@HostApi(dartHostTestHandler: 'TestCameraSelectorHostApi') +abstract class CameraSelectorHostApi { + void create(int identifier, int? lensFacing); + + List filter(int identifier, List cameraInfoIds); +} + +@FlutterApi() +abstract class CameraSelectorFlutterApi { + void create(int identifier, int? lensFacing); +} + +@HostApi(dartHostTestHandler: 'TestProcessCameraProviderHostApi') +abstract class ProcessCameraProviderHostApi { + @async + int getInstance(); + + List getAvailableCameraInfos(int identifier); +} + +@FlutterApi() +abstract class ProcessCameraProviderFlutterApi { + void create(int identifier); +} diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml new file mode 100644 index 000000000000..9873db1a0121 --- /dev/null +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -0,0 +1,30 @@ +name: camera_android_camerax +description: Android implementation of the camera plugin using the CameraX library. +repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_android_camerax +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 +publish_to: 'none' + +environment: + sdk: '>=2.17.0 <3.0.0' + flutter: ">=3.0.0" + +flutter: + plugin: + implements: camera + platforms: + android: + package: io.flutter.plugins.camerax + pluginClass: CameraAndroidCameraxPlugin + dartPluginClass: AndroidCameraCameraX + +dependencies: + camera_platform_interface: ^2.2.0 + flutter: + sdk: flutter + +dev_dependencies: + build_runner: ^2.1.4 + flutter_test: + sdk: flutter + mockito: ^5.1.0 + pigeon: ^3.2.6 diff --git a/packages/camera/camera_android_camerax/test/camera_info_test.dart b/packages/camera/camera_android_camerax/test/camera_info_test.dart new file mode 100644 index 000000000000..eda822b33f73 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/camera_info_test.dart @@ -0,0 +1,60 @@ +// 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_info.dart'; +import 'package:camera_android_camerax/src/camerax_library.pigeon.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'; + +@GenerateMocks([TestCameraInfoHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraInfo', () { + tearDown(() => TestCameraInfoHostApi.setup(null)); + + test('getSensorRotationDegreesTest', () async { + final MockTestCameraInfoHostApi mockApi = MockTestCameraInfoHostApi(); + TestCameraInfoHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final CameraInfo cameraInfo = CameraInfo.detached( + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance( + cameraInfo, + 0, + onCopy: (_) => CameraInfo.detached(), + ); + + when(mockApi.getSensorRotationDegrees( + instanceManager.getIdentifier(cameraInfo))) + .thenReturn(90); + expect(await cameraInfo.getSensorRotationDegrees(), equals(90)); + + verify(mockApi.getSensorRotationDegrees(0)); + }); + + test('flutterApiCreateTest', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final CameraInfoFlutterApi flutterApi = CameraInfoFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create(0); + + expect( + instanceManager.getInstanceWithWeakReference(0), isA()); + }); + }); +} 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 new file mode 100644 index 000000000000..e1f1e3ca9e9b --- /dev/null +++ b/packages/camera/camera_android_camerax/test/camera_info_test.mocks.dart @@ -0,0 +1,34 @@ +// Mocks generated by Mockito 5.3.0 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; + +// 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 [TestCameraInfoHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestCameraInfoHostApi extends _i1.Mock + implements _i2.TestCameraInfoHostApi { + MockTestCameraInfoHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + int getSensorRotationDegrees(int? identifier) => (super.noSuchMethod( + 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 new file mode 100644 index 000000000000..c4ccd6262376 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/camera_selector_test.dart @@ -0,0 +1,121 @@ +// 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_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/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'; + +@GenerateMocks([TestCameraSelectorHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraSelector', () { + tearDown(() => TestCameraSelectorHostApi.setup(null)); + + test('detachedCreateTest', () async { + final MockTestCameraSelectorHostApi mockApi = + MockTestCameraSelectorHostApi(); + TestCameraSelectorHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + CameraSelector.detached( + instanceManager: instanceManager, + ); + + verifyNever(mockApi.create(argThat(isA()), null)); + }); + + test('createTestWithoutLensSpecified', () async { + final MockTestCameraSelectorHostApi mockApi = + MockTestCameraSelectorHostApi(); + TestCameraSelectorHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + CameraSelector( + instanceManager: instanceManager, + ); + + verify(mockApi.create(argThat(isA()), null)); + }); + + test('createTestWithLensSpecified', () async { + final MockTestCameraSelectorHostApi mockApi = + MockTestCameraSelectorHostApi(); + TestCameraSelectorHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + CameraSelector( + instanceManager: instanceManager, + lensFacing: CameraSelector.LENS_FACING_BACK); + + verify( + mockApi.create(argThat(isA()), CameraSelector.LENS_FACING_BACK)); + }); + + test('filterTest', () async { + final MockTestCameraSelectorHostApi mockApi = + MockTestCameraSelectorHostApi(); + TestCameraSelectorHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final CameraSelector cameraSelector = CameraSelector.detached( + instanceManager: instanceManager, + ); + const int cameraInfoId = 3; + final CameraInfo cameraInfo = + CameraInfo.detached(instanceManager: instanceManager); + + instanceManager.addHostCreatedInstance( + cameraSelector, + 0, + onCopy: (_) => CameraSelector.detached(), + ); + instanceManager.addHostCreatedInstance( + cameraInfo, + cameraInfoId, + onCopy: (_) => CameraInfo.detached(), + ); + + when(mockApi.filter(instanceManager.getIdentifier(cameraSelector), + [cameraInfoId])).thenReturn([cameraInfoId]); + expect(await cameraSelector.filter([cameraInfo]), + equals([cameraInfo])); + + verify(mockApi.filter(0, [cameraInfoId])); + }); + + test('flutterApiCreateTest', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final CameraSelectorFlutterApi flutterApi = CameraSelectorFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create(0, CameraSelector.LENS_FACING_BACK); + + expect(instanceManager.getInstanceWithWeakReference(0), + isA()); + expect( + (instanceManager.getInstanceWithWeakReference(0)! as CameraSelector) + .lensFacing, + equals(CameraSelector.LENS_FACING_BACK)); + }); + }); +} 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 new file mode 100644 index 000000000000..456db1eaf822 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/camera_selector_test.mocks.dart @@ -0,0 +1,38 @@ +// Mocks generated by Mockito 5.3.0 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; + +// 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 [TestCameraSelectorHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestCameraSelectorHostApi extends _i1.Mock + implements _i2.TestCameraSelectorHostApi { + MockTestCameraSelectorHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + 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); +} diff --git a/packages/camera/camera_android_camerax/test/instance_manager_test.dart b/packages/camera/camera_android_camerax/test/instance_manager_test.dart new file mode 100644 index 000000000000..9562c41674ae --- /dev/null +++ b/packages/camera/camera_android_camerax/test/instance_manager_test.dart @@ -0,0 +1,174 @@ +// 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/instance_manager.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('InstanceManager', () { + test('addHostCreatedInstance', () { + final Object object = Object(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance( + object, + 0, + onCopy: (_) => Object(), + ); + + expect(instanceManager.getIdentifier(object), 0); + expect( + instanceManager.getInstanceWithWeakReference(0), + object, + ); + }); + + test('addHostCreatedInstance prevents already used objects and ids', () { + final Object object = Object(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance( + object, + 0, + onCopy: (_) => Object(), + ); + + expect( + () => instanceManager.addHostCreatedInstance( + object, + 0, + onCopy: (_) => Object(), + ), + throwsAssertionError, + ); + + expect( + () => instanceManager.addHostCreatedInstance( + Object(), + 0, + onCopy: (_) => Object(), + ), + throwsAssertionError, + ); + }); + + test('addDartCreatedInstance', () { + final Object object = Object(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addDartCreatedInstance( + object, + onCopy: (_) => Object(), + ); + + final int? instanceId = instanceManager.getIdentifier(object); + expect(instanceId, isNotNull); + expect( + instanceManager.getInstanceWithWeakReference(instanceId!), + object, + ); + }); + + test('removeWeakReference', () { + final Object object = Object(); + + int? weakInstanceId; + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (int instanceId) { + weakInstanceId = instanceId; + }); + + instanceManager.addHostCreatedInstance( + object, + 0, + onCopy: (_) => Object(), + ); + + expect(instanceManager.removeWeakReference(object), 0); + expect( + instanceManager.getInstanceWithWeakReference(0), + isA(), + ); + expect(weakInstanceId, 0); + }); + + test('removeWeakReference removes only weak reference', () { + final Object object = Object(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance( + object, + 0, + onCopy: (_) => Object(), + ); + + expect(instanceManager.removeWeakReference(object), 0); + final Object copy = instanceManager.getInstanceWithWeakReference( + 0, + )!; + expect(identical(object, copy), isFalse); + }); + + test('removeStrongReference', () { + final Object object = Object(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance( + object, + 0, + onCopy: (_) => Object(), + ); + instanceManager.removeWeakReference(object); + expect(instanceManager.remove(0), isA()); + expect(instanceManager.containsIdentifier(0), isFalse); + }); + + test('removeStrongReference removes only strong reference', () { + final Object object = Object(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance( + object, + 0, + onCopy: (_) => Object(), + ); + expect(instanceManager.remove(0), isA()); + expect( + instanceManager.getInstanceWithWeakReference(0), + object, + ); + }); + + test('getInstance can add a new weak reference', () { + final Object object = Object(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance( + object, + 0, + onCopy: (_) => Object(), + ); + instanceManager.removeWeakReference(object); + + final Object newWeakCopy = instanceManager.getInstanceWithWeakReference( + 0, + )!; + expect(identical(object, newWeakCopy), isFalse); + }); + }); +} 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 new file mode 100644 index 000000000000..65e7d00ddaea --- /dev/null +++ b/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart @@ -0,0 +1,96 @@ +// 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_info.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/process_camera_provider.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'; + +@GenerateMocks([TestProcessCameraProviderHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('ProcessCameraProvider', () { + tearDown(() => TestProcessCameraProviderHostApi.setup(null)); + + test('getInstanceTest', () async { + final MockTestProcessCameraProviderHostApi mockApi = + MockTestProcessCameraProviderHostApi(); + TestProcessCameraProviderHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ProcessCameraProvider processCameraProvider = + ProcessCameraProvider.detached( + instanceManager: instanceManager, + ); + + instanceManager.addHostCreatedInstance( + processCameraProvider, + 0, + onCopy: (_) => ProcessCameraProvider.detached(), + ); + + when(mockApi.getInstance()).thenAnswer((_) async => 0); + expect( + await ProcessCameraProvider.getInstance( + instanceManager: instanceManager), + equals(processCameraProvider)); + verify(mockApi.getInstance()); + }); + + test('getAvailableCameraInfosTest', () async { + final MockTestProcessCameraProviderHostApi mockApi = + MockTestProcessCameraProviderHostApi(); + TestProcessCameraProviderHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ProcessCameraProvider processCameraProvider = + ProcessCameraProvider.detached( + instanceManager: instanceManager, + ); + + instanceManager.addHostCreatedInstance( + processCameraProvider, + 0, + onCopy: (_) => ProcessCameraProvider.detached(), + ); + final CameraInfo fakeAvailableCameraInfo = + CameraInfo.detached(instanceManager: instanceManager); + instanceManager.addHostCreatedInstance( + fakeAvailableCameraInfo, + 1, + onCopy: (_) => CameraInfo.detached(), + ); + + when(mockApi.getAvailableCameraInfos(0)).thenReturn([1]); + expect(await processCameraProvider.getAvailableCameraInfos(), + equals([fakeAvailableCameraInfo])); + verify(mockApi.getAvailableCameraInfos(0)); + }); + + test('flutterApiCreateTest', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ProcessCameraProviderFlutterApiImpl flutterApi = + ProcessCameraProviderFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create(0); + + expect(instanceManager.getInstanceWithWeakReference(0), + isA()); + }); + }); +} 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 new file mode 100644 index 000000000000..9fcfe690c062 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/process_camera_provider_test.mocks.dart @@ -0,0 +1,40 @@ +// Mocks generated by Mockito 5.3.0 from annotations +// in camera_android_camerax/test/process_camera_provider_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.pigeon.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 [TestProcessCameraProviderHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestProcessCameraProviderHostApi extends _i1.Mock + implements _i2.TestProcessCameraProviderHostApi { + MockTestProcessCameraProviderHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + _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); +} 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 new file mode 100644 index 000000000000..2196b73d7fdb --- /dev/null +++ b/packages/camera/camera_android_camerax/test/test_camerax_library.pigeon.dart @@ -0,0 +1,187 @@ +// 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/AUTHORS b/packages/camera/camera_avfoundation/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/camera/camera_avfoundation/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md new file mode 100644 index 000000000000..12d9a53ea248 --- /dev/null +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -0,0 +1,33 @@ +## 0.9.8+6 + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. +* Updates minimum Flutter version to 2.10. + +## 0.9.8+5 + +* Fixes a regression introduced in 0.9.8+4 where the stream handler is not set. + +## 0.9.8+4 + +* Fixes a crash due to sending orientation change events when the engine is torn down. + +## 0.9.8+3 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. +* Ignores missing return warnings in preparation for [upcoming analysis changes](https://github.com/flutter/flutter/issues/105750). + +## 0.9.8+2 + +* Fixes exception in registerWith caused by the switch to an in-package method channel. + +## 0.9.8+1 + +* Ignores deprecation warnings for upcoming styleFrom button API changes. + +## 0.9.8 + +* Switches to internal method channel implementation. + +## 0.9.7+1 + +* Splits from `camera` as a federated implementation. diff --git a/packages/camera/camera_avfoundation/LICENSE b/packages/camera/camera_avfoundation/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/camera/camera_avfoundation/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/camera/camera_avfoundation/README.md b/packages/camera/camera_avfoundation/README.md new file mode 100644 index 000000000000..a063492e6c15 --- /dev/null +++ b/packages/camera/camera_avfoundation/README.md @@ -0,0 +1,11 @@ +# camera\_avfoundation + +The iOS implementation of [`camera`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `camera` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/camera +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart new file mode 100644 index 000000000000..3e62edc2c495 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart @@ -0,0 +1,256 @@ +// 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'; +import 'dart:ui'; + +import 'package:camera_avfoundation/camera_avfoundation.dart'; +import 'package:camera_example/camera_controller.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + late Directory testDir; + + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + CameraPlatform.instance = AVFoundationCamera(); + final Directory extDir = await getTemporaryDirectory(); + testDir = await Directory('${extDir.path}/test').create(recursive: true); + }); + + tearDownAll(() async { + await testDir.delete(recursive: true); + }); + + final Map presetExpectedSizes = + { + ResolutionPreset.low: const Size(288, 352), + ResolutionPreset.medium: const Size(480, 640), + ResolutionPreset.high: const Size(720, 1280), + ResolutionPreset.veryHigh: const Size(1080, 1920), + ResolutionPreset.ultraHigh: const Size(2160, 3840), + // Don't bother checking for max here since it could be anything. + }; + + /// Verify that [actual] has dimensions that are at least as large as + /// [expectedSize]. Allows for a mismatch in portrait vs landscape. Returns + /// whether the dimensions exactly match. + bool assertExpectedDimensions(Size expectedSize, Size actual) { + expect(actual.shortestSide, lessThanOrEqualTo(expectedSize.shortestSide)); + expect(actual.longestSide, lessThanOrEqualTo(expectedSize.longestSide)); + return actual.shortestSide == expectedSize.shortestSide && + actual.longestSide == expectedSize.longestSide; + } + + // This tests that the capture is no bigger than the preset, since we have + // automatic code to fall back to smaller sizes when we need to. Returns + // whether the image is exactly the desired resolution. + Future testCaptureImageResolution( + CameraController controller, ResolutionPreset preset) async { + final Size expectedSize = presetExpectedSizes[preset]!; + print( + 'Capturing photo at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); + + // Take Picture + final XFile file = await controller.takePicture(); + + // Load picture + final File fileImage = File(file.path); + final Image image = await decodeImageFromList(fileImage.readAsBytesSync()); + + // Verify image dimensions are as expected + expect(image, isNotNull); + return assertExpectedDimensions( + expectedSize, Size(image.height.toDouble(), image.width.toDouble())); + } + + testWidgets('Capture specific image resolutions', + (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + return; + } + for (final CameraDescription cameraDescription in cameras) { + bool previousPresetExactlySupported = true; + for (final MapEntry preset + in presetExpectedSizes.entries) { + final CameraController controller = + CameraController(cameraDescription, preset.key); + await controller.initialize(); + final bool presetExactlySupported = + await testCaptureImageResolution(controller, preset.key); + assert(!(!previousPresetExactlySupported && presetExactlySupported), + 'The camera took higher resolution pictures at a lower resolution.'); + previousPresetExactlySupported = presetExactlySupported; + await controller.dispose(); + } + } + }); + + // This tests that the capture is no bigger than the preset, since we have + // automatic code to fall back to smaller sizes when we need to. Returns + // whether the image is exactly the desired resolution. + Future testCaptureVideoResolution( + CameraController controller, ResolutionPreset preset) async { + final Size expectedSize = presetExpectedSizes[preset]!; + print( + 'Capturing video at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); + + // Take Video + await controller.startVideoRecording(); + sleep(const Duration(milliseconds: 300)); + final XFile file = await controller.stopVideoRecording(); + + // Load video metadata + final File videoFile = File(file.path); + final VideoPlayerController videoController = + VideoPlayerController.file(videoFile); + await videoController.initialize(); + final Size video = videoController.value.size; + + // Verify image dimensions are as expected + expect(video, isNotNull); + return assertExpectedDimensions( + expectedSize, Size(video.height, video.width)); + } + + testWidgets('Capture specific video resolutions', + (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + return; + } + for (final CameraDescription cameraDescription in cameras) { + bool previousPresetExactlySupported = true; + for (final MapEntry preset + in presetExpectedSizes.entries) { + final CameraController controller = + CameraController(cameraDescription, preset.key); + await controller.initialize(); + await controller.prepareForVideoRecording(); + final bool presetExactlySupported = + await testCaptureVideoResolution(controller, preset.key); + assert(!(!previousPresetExactlySupported && presetExactlySupported), + 'The camera took higher resolution pictures at a lower resolution.'); + previousPresetExactlySupported = presetExactlySupported; + await controller.dispose(); + } + } + }); + + testWidgets('Pause and resume video recording', (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + await controller.prepareForVideoRecording(); + + int startPause; + int timePaused = 0; + + await controller.startVideoRecording(); + final int recordingStart = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + + await controller.pauseVideoRecording(); + startPause = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + await controller.resumeVideoRecording(); + timePaused += DateTime.now().millisecondsSinceEpoch - startPause; + + sleep(const Duration(milliseconds: 500)); + + await controller.pauseVideoRecording(); + startPause = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + await controller.resumeVideoRecording(); + timePaused += DateTime.now().millisecondsSinceEpoch - startPause; + + sleep(const Duration(milliseconds: 500)); + + final XFile file = await controller.stopVideoRecording(); + final int recordingTime = + DateTime.now().millisecondsSinceEpoch - recordingStart; + + final File videoFile = File(file.path); + final VideoPlayerController videoController = VideoPlayerController.file( + videoFile, + ); + await videoController.initialize(); + final int duration = videoController.value.duration.inMilliseconds; + await videoController.dispose(); + + expect(duration, lessThan(recordingTime - timePaused)); + }); + + /// Start streaming with specifying the ImageFormatGroup. + Future startStreaming(List cameras, + ImageFormatGroup? imageFormatGroup) async { + final CameraController controller = CameraController( + cameras.first, + ResolutionPreset.low, + enableAudio: false, + imageFormatGroup: imageFormatGroup, + ); + + await controller.initialize(); + final Completer completer = Completer(); + + await controller.startImageStream((CameraImageData image) { + if (!completer.isCompleted) { + Future(() async { + await controller.stopImageStream(); + await controller.dispose(); + }).then((Object? value) { + completer.complete(image); + }); + } + }); + return completer.future; + } + + testWidgets( + 'image streaming with imageFormatGroup', + (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + return; + } + + CameraImageData image = await startStreaming(cameras, null); + expect(image, isNotNull); + expect(image.format.group, ImageFormatGroup.bgra8888); + expect(image.planes.length, 1); + + image = await startStreaming(cameras, ImageFormatGroup.yuv420); + expect(image, isNotNull); + expect(image.format.group, ImageFormatGroup.yuv420); + expect(image.planes.length, 2); + + image = await startStreaming(cameras, ImageFormatGroup.bgra8888); + expect(image, isNotNull); + expect(image.format.group, ImageFormatGroup.bgra8888); + expect(image.planes.length, 1); + }, + ); +} diff --git a/packages/camera/camera_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist b/packages/camera/camera_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 9.0 + + diff --git a/packages/camera/camera_avfoundation/example/ios/Flutter/Debug.xcconfig b/packages/camera/camera_avfoundation/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..b2f5fae9c254 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/camera/camera_avfoundation/example/ios/Flutter/Release.xcconfig b/packages/camera/camera_avfoundation/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..88c29144c836 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/camera/camera_avfoundation/example/ios/Podfile b/packages/camera/camera_avfoundation/example/ios/Podfile new file mode 100644 index 000000000000..5bc7b7e85717 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Podfile @@ -0,0 +1,45 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + target 'RunnerTests' do + platform :ios, '9.0' + inherit! :search_paths + # Pods for testing + pod 'OCMock', '~> 3.8.1' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..03c80d79c578 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,712 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */; }; + 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB766A2665316900CE5A93 /* CameraFocusTests.m */; }; + 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 236906D1621AE863A5B2E770 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 89D82918721FABF772705DB0 /* libPods-Runner.a */; }; + 25C3919135C3D981E6F800D0 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */; }; + 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 43ED1537282570DE00EB00DE /* AvailableCamerasTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 43ED1536282570DE00EB00DE /* AvailableCamerasTest.m */; }; + 788A065A27B0E02900533D74 /* StreamingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 788A065927B0E02900533D74 /* StreamingTest.m */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + E01EE4A82799F3A5008C1950 /* QueueUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E01EE4A72799F3A5008C1950 /* QueueUtilsTests.m */; }; + E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */; }; + E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */; }; + E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */; }; + E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */; }; + E0B0D2BB27DFF2AF00E71E4B /* CameraPermissionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0B0D2BA27DFF2AF00E71E4B /* CameraPermissionTests.m */; }; + E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */; }; + E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */; }; + E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */; }; + E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = E0CDBAC127CD9729002561D9 /* CameraTestUtils.m */; }; + E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */; }; + E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; }; + F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */ = {isa = PBXBuildFile; fileRef = F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 03BB766D2665316900CE5A93 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraMethodChannelTests.m; sourceTree = ""; }; + 03BB76682665316900CE5A93 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 03BB766A2665316900CE5A93 /* CameraFocusTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraFocusTests.m; sourceTree = ""; }; + 03BB766C2665316900CE5A93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraOrientationTests.m; sourceTree = ""; }; + 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeFlutterResultTests.m; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 14AE82C910C2A12F2ECB2094 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 43ED1536282570DE00EB00DE /* AvailableCamerasTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AvailableCamerasTest.m; sourceTree = ""; }; + 59848A7CA98C1FADF8840207 /* 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 = ""; }; + 788A065927B0E02900533D74 /* StreamingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StreamingTest.m; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 89D82918721FABF772705DB0 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9C5CC6CAD53AD388B2694F3A /* 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 = ""; }; + A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + E01EE4A72799F3A5008C1950 /* QueueUtilsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QueueUtilsTests.m; sourceTree = ""; }; + E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraCaptureSessionQueueRaceConditionTests.m; sourceTree = ""; }; + E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTSavePhotoDelegateTests.m; sourceTree = ""; }; + E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTCamPhotoCaptureTests.m; sourceTree = ""; }; + E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTCamSampleBufferTests.m; sourceTree = ""; }; + E0B0D2BA27DFF2AF00E71E4B /* CameraPermissionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPermissionTests.m; sourceTree = ""; }; + E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = ""; }; + E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = ""; }; + E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = ""; }; + E0CDBAC027CD9729002561D9 /* CameraTestUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CameraTestUtils.h; sourceTree = ""; }; + E0CDBAC127CD9729002561D9 /* CameraTestUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraTestUtils.m; sourceTree = ""; }; + E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPropertiesTests.m; sourceTree = ""; }; + E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = ""; }; + F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFLTThreadSafeFlutterResult.h; sourceTree = ""; }; + F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFLTThreadSafeFlutterResult.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 03BB76652665316900CE5A93 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 25C3919135C3D981E6F800D0 /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 236906D1621AE863A5B2E770 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 03BB76692665316900CE5A93 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 03BB766A2665316900CE5A93 /* CameraFocusTests.m */, + 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */, + 03BB766C2665316900CE5A93 /* Info.plist */, + 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */, + 03F6F8B126CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m */, + E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */, + E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */, + E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */, + E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */, + E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */, + E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */, + E0B0D2BA27DFF2AF00E71E4B /* CameraPermissionTests.m */, + E01EE4A72799F3A5008C1950 /* QueueUtilsTests.m */, + E0CDBAC027CD9729002561D9 /* CameraTestUtils.h */, + E0CDBAC127CD9729002561D9 /* CameraTestUtils.m */, + E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, + F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */, + F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */, + E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */, + E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */, + 788A065927B0E02900533D74 /* StreamingTest.m */, + 43ED1536282570DE00EB00DE /* AvailableCamerasTest.m */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 3242FD2B467C15C62200632F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 89D82918721FABF772705DB0 /* libPods-Runner.a */, + 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 03BB76692665316900CE5A93 /* RunnerTests */, + 97C146EF1CF9000F007C117D /* Products */, + FD386F00E98D73419C929072 /* Pods */, + 3242FD2B467C15C62200632F /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 03BB76682665316900CE5A93 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + FD386F00E98D73419C929072 /* Pods */ = { + isa = PBXGroup; + children = ( + 59848A7CA98C1FADF8840207 /* Pods-Runner.debug.xcconfig */, + 14AE82C910C2A12F2ECB2094 /* Pods-Runner.release.xcconfig */, + 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */, + A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 03BB76672665316900CE5A93 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03BB76712665316900CE5A93 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 422786A96136AA9087A2041B /* [CP] Check Pods Manifest.lock */, + 03BB76642665316900CE5A93 /* Sources */, + 03BB76652665316900CE5A93 /* Frameworks */, + 03BB76662665316900CE5A93 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 03BB766E2665316900CE5A93 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = camera_exampleTests; + productReference = 03BB76682665316900CE5A93 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9872F2A25E8A171A111468CD /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 03BB76672665316900CE5A93 = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 03BB76672665316900CE5A93 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 03BB76662665316900CE5A93 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 422786A96136AA9087A2041B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + 9872F2A25E8A171A111468CD /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 03BB76642665316900CE5A93 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */, + 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */, + E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */, + E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */, + 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, + E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, + E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */, + E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */, + 43ED1537282570DE00EB00DE /* AvailableCamerasTest.m in Sources */, + F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */, + E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */, + 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, + E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */, + 788A065A27B0E02900533D74 /* StreamingTest.m in Sources */, + E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */, + E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */, + E0B0D2BB27DFF2AF00E71E4B /* CameraPermissionTests.m in Sources */, + E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */, + E01EE4A82799F3A5008C1950 /* QueueUtilsTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 03BB766E2665316900CE5A93 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 03BB766D2665316900CE5A93 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 03BB766F2665316900CE5A93 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "dev.flutter.plugins.cameraExample.camera-exampleTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + 03BB76702665316900CE5A93 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "dev.flutter.plugins.cameraExample.camera-exampleTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.cameraExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.cameraExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 03BB76712665316900CE5A93 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03BB766F2665316900CE5A93 /* Debug */, + 03BB76702665316900CE5A93 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..f4b3c1099001 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/camera/camera_avfoundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/camera/camera_avfoundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/AppDelegate.h b/packages/camera/camera_avfoundation/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner/AppDelegate.h @@ -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 +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/AppDelegate.m b/packages/camera/camera_avfoundation/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner/AppDelegate.m @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d225b3c2cfe2 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,121 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/camera/camera_avfoundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Base.lproj/Main.storyboard b/packages/camera/camera_avfoundation/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist b/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..ff2e341a1803 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist @@ -0,0 +1,56 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + camera_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSApplicationCategoryType + + LSRequiresIPhoneOS + + NSCameraUsageDescription + Can I use the camera please? Only for demo purpose of the app + NSMicrophoneUsageDescription + Only for demo purpose of the app + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/main.m b/packages/camera/camera_avfoundation/example/ios/Runner/main.m new file mode 100644 index 000000000000..d1224fea37ed --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/Runner/main.m @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + // The setup logic in `AppDelegate::didFinishLaunchingWithOptions:` eventually sends camera + // operations on the background queue, which would run concurrently with the test cases during + // unit tests, making the debugging process confusing. This setup is actually not necessary for + // the unit tests, so it is better to skip the AppDelegate when running unit tests. + BOOL isTesting = NSClassFromString(@"XCTestCase") != nil; + return UIApplicationMain(argc, argv, nil, + isTesting ? nil : NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTest.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTest.m new file mode 100644 index 000000000000..6074b871cd02 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTest.m @@ -0,0 +1,121 @@ +// 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 camera_avfoundation; +@import camera_avfoundation.Test; +@import XCTest; +@import AVFoundation; +#import +#import "MockFLTThreadSafeFlutterResult.h" + +@interface AvailableCamerasTest : XCTestCase +@end + +@implementation AvailableCamerasTest + +- (void)testAvailableCamerasShouldReturnAllCamerasOnMultiCameraIPhone { + CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + // iPhone 13 Cameras: + AVCaptureDevice *wideAngleCamera = OCMClassMock([AVCaptureDevice class]); + OCMStub([wideAngleCamera uniqueID]).andReturn(@"0"); + OCMStub([wideAngleCamera position]).andReturn(AVCaptureDevicePositionBack); + + AVCaptureDevice *frontFacingCamera = OCMClassMock([AVCaptureDevice class]); + OCMStub([frontFacingCamera uniqueID]).andReturn(@"1"); + OCMStub([frontFacingCamera position]).andReturn(AVCaptureDevicePositionFront); + + AVCaptureDevice *ultraWideCamera = OCMClassMock([AVCaptureDevice class]); + OCMStub([ultraWideCamera uniqueID]).andReturn(@"2"); + OCMStub([ultraWideCamera position]).andReturn(AVCaptureDevicePositionBack); + + AVCaptureDevice *telephotoCamera = OCMClassMock([AVCaptureDevice class]); + OCMStub([telephotoCamera uniqueID]).andReturn(@"3"); + OCMStub([telephotoCamera position]).andReturn(AVCaptureDevicePositionBack); + + NSMutableArray *requiredTypes = + [@[ AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeBuiltInTelephotoCamera ] + mutableCopy]; + if (@available(iOS 13.0, *)) { + [requiredTypes addObject:AVCaptureDeviceTypeBuiltInUltraWideCamera]; + } + + id discoverySessionMock = OCMClassMock([AVCaptureDeviceDiscoverySession class]); + OCMStub([discoverySessionMock discoverySessionWithDeviceTypes:requiredTypes + mediaType:AVMediaTypeVideo + position:AVCaptureDevicePositionUnspecified]) + .andReturn(discoverySessionMock); + + NSMutableArray *cameras = [NSMutableArray array]; + [cameras addObjectsFromArray:@[ wideAngleCamera, frontFacingCamera, telephotoCamera ]]; + if (@available(iOS 13.0, *)) { + [cameras addObject:ultraWideCamera]; + } + OCMStub([discoverySessionMock devices]).andReturn([NSArray arrayWithArray:cameras]); + + MockFLTThreadSafeFlutterResult *resultObject = + [[MockFLTThreadSafeFlutterResult alloc] initWithExpectation:expectation]; + + // Set up method call + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"availableCameras" + arguments:nil]; + + [camera handleMethodCallAsync:call result:resultObject]; + + // Verify the result + NSDictionary *dictionaryResult = (NSDictionary *)resultObject.receivedResult; + if (@available(iOS 13.0, *)) { + XCTAssertTrue([dictionaryResult count] == 4); + } else { + XCTAssertTrue([dictionaryResult count] == 3); + } +} +- (void)testAvailableCamerasShouldReturnOneCameraOnSingleCameraIPhone { + CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + + // iPhone 8 Cameras: + AVCaptureDevice *wideAngleCamera = OCMClassMock([AVCaptureDevice class]); + OCMStub([wideAngleCamera uniqueID]).andReturn(@"0"); + OCMStub([wideAngleCamera position]).andReturn(AVCaptureDevicePositionBack); + + AVCaptureDevice *frontFacingCamera = OCMClassMock([AVCaptureDevice class]); + OCMStub([frontFacingCamera uniqueID]).andReturn(@"1"); + OCMStub([frontFacingCamera position]).andReturn(AVCaptureDevicePositionFront); + + NSMutableArray *requiredTypes = + [@[ AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeBuiltInTelephotoCamera ] + mutableCopy]; + if (@available(iOS 13.0, *)) { + [requiredTypes addObject:AVCaptureDeviceTypeBuiltInUltraWideCamera]; + } + + id discoverySessionMock = OCMClassMock([AVCaptureDeviceDiscoverySession class]); + OCMStub([discoverySessionMock discoverySessionWithDeviceTypes:requiredTypes + mediaType:AVMediaTypeVideo + position:AVCaptureDevicePositionUnspecified]) + .andReturn(discoverySessionMock); + + NSMutableArray *cameras = [NSMutableArray array]; + [cameras addObjectsFromArray:@[ wideAngleCamera, frontFacingCamera ]]; + OCMStub([discoverySessionMock devices]).andReturn([NSArray arrayWithArray:cameras]); + + MockFLTThreadSafeFlutterResult *resultObject = + [[MockFLTThreadSafeFlutterResult alloc] initWithExpectation:expectation]; + + // Set up method call + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"availableCameras" + arguments:nil]; + + [camera handleMethodCallAsync:call result:resultObject]; + + // Verify the result + NSDictionary *dictionaryResult = (NSDictionary *)resultObject.receivedResult; + XCTAssertTrue([dictionaryResult count] == 2); +} + +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m similarity index 79% rename from packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m index 667a122d9375..89f40307933c 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; -@import camera.Test; +@import camera_avfoundation; +@import camera_avfoundation.Test; @import XCTest; @interface CameraCaptureSessionQueueRaceConditionTests : XCTestCase @@ -29,10 +29,11 @@ - (void)testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition { result:^(id _Nullable result) { [disposeExpectation fulfill]; }]; - [camera handleMethodCall:createCall - result:^(id _Nullable result) { - [createExpectation fulfill]; - }]; + [camera createCameraOnSessionQueueWithCreateMethodCall:createCall + result:[[FLTThreadSafeFlutterResult alloc] + initWithResult:^(id _Nullable result) { + [createExpectation fulfill]; + }]]; [self waitForExpectationsWithTimeout:1 handler:nil]; // `captureSessionQueue` must not be nil after `create` call. Otherwise a nil // `captureSessionQueue` passed into `AVCaptureVideoDataOutput::setSampleBufferDelegate:queue:` diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraExposureTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraExposureTests.m similarity index 98% rename from packages/camera/camera/example/ios/RunnerTests/CameraExposureTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraExposureTests.m index ee43d3f155f4..7b641a5746c0 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraExposureTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraExposureTests.m @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; +@import camera_avfoundation; @import XCTest; @import AVFoundation; #import diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraFocusTests.m similarity index 98% rename from packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraFocusTests.m index e0f5fdaa3c97..1b6ada564dd8 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraFocusTests.m @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; -@import camera.Test; +@import camera_avfoundation; +@import camera_avfoundation.Test; @import XCTest; @import AVFoundation; #import diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.m similarity index 84% rename from packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.m index 254a33c7ee4e..bd20134db561 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; -@import camera.Test; +@import camera_avfoundation; +@import camera_avfoundation.Test; @import XCTest; @import AVFoundation; #import @@ -17,8 +17,7 @@ @implementation CameraMethodChannelTests - (void)testCreate_ShouldCallResultOnMainThread { CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; - XCTestExpectation *expectation = - [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; // Set up mocks for initWithCameraName method id avCaptureDeviceInputMock = OCMClassMock([AVCaptureDeviceInput class]); @@ -37,7 +36,8 @@ - (void)testCreate_ShouldCallResultOnMainThread { methodCallWithMethodName:@"create" arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; - [camera handleMethodCallAsync:call result:resultObject]; + [camera createCameraOnSessionQueueWithCreateMethodCall:call result:resultObject]; + [self waitForExpectationsWithTimeout:1 handler:nil]; // Verify the result NSDictionary *dictionaryResult = (NSDictionary *)resultObject.receivedResult; diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m similarity index 74% rename from packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m index 50f3416e7869..60e88fffee2b 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; -@import camera.Test; +@import camera_avfoundation; +@import camera_avfoundation.Test; @import XCTest; @import Flutter; @@ -92,6 +92,39 @@ - (void)rotate:(UIDeviceOrientation)deviceOrientation [self waitForExpectationsWithTimeout:30.0 handler:nil]; } +- (void)testOrientationChanged_noRetainCycle { + dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); + FLTCam *mockCam = OCMClassMock([FLTCam class]); + FLTThreadSafeMethodChannel *mockChannel = OCMClassMock([FLTThreadSafeMethodChannel class]); + + __weak CameraPlugin *weakCamera; + + @autoreleasepool { + CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + weakCamera = camera; + camera.captureSessionQueue = captureSessionQueue; + camera.camera = mockCam; + camera.deviceEventMethodChannel = mockChannel; + + [camera orientationChanged: + [self createMockNotificationForOrientation:UIDeviceOrientationLandscapeLeft]]; + } + + // Sanity check + XCTAssertNil(weakCamera, @"Camera must have been deallocated."); + + // Must check in captureSessionQueue since orientationChanged dispatches to this queue. + XCTestExpectation *expectation = + [self expectationWithDescription:@"Dispatched to capture session queue"]; + dispatch_async(captureSessionQueue, ^{ + OCMVerify(never(), [mockCam setDeviceOrientation:UIDeviceOrientationLandscapeLeft]); + OCMVerify(never(), [mockChannel invokeMethod:@"orientation_changed" arguments:OCMOCK_ANY]); + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + - (NSNotification *)createMockNotificationForOrientation:(UIDeviceOrientation)deviceOrientation { UIDevice *mockDevice = OCMClassMock([UIDevice class]); OCMStub([mockDevice orientation]).andReturn(deviceOrientation); diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPermissionTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPermissionTests.m new file mode 100644 index 000000000000..24ca5b6525c9 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPermissionTests.m @@ -0,0 +1,231 @@ +// 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 camera_avfoundation; +@import camera_avfoundation.Test; +@import AVFoundation; +@import XCTest; +#import +#import "CameraTestUtils.h" + +@interface CameraPermissionTests : XCTestCase + +@end + +@implementation CameraPermissionTests + +#pragma mark - camera permissions + +- (void)testRequestCameraPermission_completeWithoutErrorIfPrevoiuslyAuthorized { + XCTestExpectation *expectation = + [self expectationWithDescription: + @"Must copmlete without error if camera access was previously authorized."]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusAuthorized); + + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + if (error == nil) { + [expectation fulfill]; + } + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} +- (void)testRequestCameraPermission_completeWithErrorIfPreviouslyDenied { + XCTestExpectation *expectation = + [self expectationWithDescription: + @"Must complete with error if camera access was previously denied."]; + FlutterError *expectedError = + [FlutterError errorWithCode:@"CameraAccessDeniedWithoutPrompt" + message:@"User has previously denied the camera access request. Go to " + @"Settings to enable camera access." + details:nil]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusDenied); + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + if ([error isEqual:expectedError]) { + [expectation fulfill]; + } + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRequestCameraPermission_completeWithErrorIfRestricted { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Must complete with error if camera access is restricted."]; + FlutterError *expectedError = [FlutterError errorWithCode:@"CameraAccessRestricted" + message:@"Camera access is restricted. " + details:nil]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusRestricted); + + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + if ([error isEqual:expectedError]) { + [expectation fulfill]; + } + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRequestCameraPermission_completeWithoutErrorIfUserGrantAccess { + XCTestExpectation *grantedExpectation = [self + expectationWithDescription:@"Must complete without error if user choose to grant access"]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusNotDetermined); + // Mimic user choosing "allow" in permission dialog. + OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeVideo + completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) { + block(YES); + return YES; + }]]); + + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + if (error == nil) { + [grantedExpectation fulfill]; + } + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRequestCameraPermission_completeWithErrorIfUserDenyAccess { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Must complete with error if user choose to deny access"]; + FlutterError *expectedError = + [FlutterError errorWithCode:@"CameraAccessDenied" + message:@"User denied the camera access request." + details:nil]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusNotDetermined); + + // Mimic user choosing "deny" in permission dialog. + OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeVideo + completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) { + block(NO); + return YES; + }]]); + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + if ([error isEqual:expectedError]) { + [expectation fulfill]; + } + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +#pragma mark - audio permissions + +- (void)testRequestAudioPermission_completeWithoutErrorIfPrevoiuslyAuthorized { + XCTestExpectation *expectation = + [self expectationWithDescription: + @"Must copmlete without error if audio access was previously authorized."]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) + .andReturn(AVAuthorizationStatusAuthorized); + + FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { + if (error == nil) { + [expectation fulfill]; + } + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} +- (void)testRequestAudioPermission_completeWithErrorIfPreviouslyDenied { + XCTestExpectation *expectation = + [self expectationWithDescription: + @"Must complete with error if audio access was previously denied."]; + FlutterError *expectedError = + [FlutterError errorWithCode:@"AudioAccessDeniedWithoutPrompt" + message:@"User has previously denied the audio access request. Go to " + @"Settings to enable audio access." + details:nil]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) + .andReturn(AVAuthorizationStatusDenied); + FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { + if ([error isEqual:expectedError]) { + [expectation fulfill]; + } + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRequestAudioPermission_completeWithErrorIfRestricted { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Must complete with error if audio access is restricted."]; + FlutterError *expectedError = [FlutterError errorWithCode:@"AudioAccessRestricted" + message:@"Audio access is restricted. " + details:nil]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) + .andReturn(AVAuthorizationStatusRestricted); + + FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { + if ([error isEqual:expectedError]) { + [expectation fulfill]; + } + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRequestAudioPermission_completeWithoutErrorIfUserGrantAccess { + XCTestExpectation *grantedExpectation = [self + expectationWithDescription:@"Must complete without error if user choose to grant access"]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) + .andReturn(AVAuthorizationStatusNotDetermined); + // Mimic user choosing "allow" in permission dialog. + OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio + completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) { + block(YES); + return YES; + }]]); + + FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { + if (error == nil) { + [grantedExpectation fulfill]; + } + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRequestAudioPermission_completeWithErrorIfUserDenyAccess { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Must complete with error if user choose to deny access"]; + FlutterError *expectedError = [FlutterError errorWithCode:@"AudioAccessDenied" + message:@"User denied the audio access request." + details:nil]; + + id mockDevice = OCMClassMock([AVCaptureDevice class]); + OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) + .andReturn(AVAuthorizationStatusNotDetermined); + + // Mimic user choosing "deny" in permission dialog. + OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio + completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) { + block(NO); + return YES; + }]]); + FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { + if ([error isEqual:expectedError]) { + [expectation fulfill]; + } + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPreviewPauseTests.m similarity index 93% rename from packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPreviewPauseTests.m index 2c1adbef468b..1dfc90b27f1b 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPreviewPauseTests.m @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; -@import camera.Test; +@import camera_avfoundation; +@import camera_avfoundation.Test; @import XCTest; @import AVFoundation; #import diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPropertiesTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m similarity index 99% rename from packages/camera/camera/example/ios/RunnerTests/CameraPropertiesTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m index 865791586382..18c01e599907 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraPropertiesTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera.Test; +@import camera_avfoundation.Test; @import AVFoundation; @import XCTest; diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.h new file mode 100644 index 000000000000..f2d46114a0c5 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.h @@ -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 camera_avfoundation; + +NS_ASSUME_NONNULL_BEGIN + +/// Creates an `FLTCam` that runs its capture session operations on a given queue. +/// @param captureSessionQueue the capture session queue +/// @return an FLTCam object. +extern FLTCam *FLTCreateCamWithCaptureSessionQueue(dispatch_queue_t captureSessionQueue); + +/// Creates a test sample buffer. +/// @return a test sample buffer. +extern CMSampleBufferRef FLTCreateTestSampleBuffer(void); + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m new file mode 100644 index 000000000000..0ae4887eb631 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m @@ -0,0 +1,44 @@ +// 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 "CameraTestUtils.h" +#import +@import AVFoundation; + +FLTCam *FLTCreateCamWithCaptureSessionQueue(dispatch_queue_t captureSessionQueue) { + id inputMock = OCMClassMock([AVCaptureDeviceInput class]); + OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) + .andReturn(inputMock); + + id sessionMock = OCMClassMock([AVCaptureSession class]); + OCMStub([sessionMock addInputWithNoConnections:[OCMArg any]]); // no-op + OCMStub([sessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + + return [[FLTCam alloc] initWithCameraName:@"camera" + resolutionPreset:@"medium" + enableAudio:true + orientation:UIDeviceOrientationPortrait + captureSession:sessionMock + captureSessionQueue:captureSessionQueue + error:nil]; +} + +CMSampleBufferRef FLTCreateTestSampleBuffer(void) { + CVPixelBufferRef pixelBuffer; + CVPixelBufferCreate(kCFAllocatorDefault, 100, 100, kCVPixelFormatType_32BGRA, NULL, &pixelBuffer); + + CMFormatDescriptionRef formatDescription; + CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, + &formatDescription); + + CMSampleTimingInfo timingInfo = {CMTimeMake(1, 44100), kCMTimeZero, kCMTimeInvalid}; + + CMSampleBufferRef sampleBuffer; + CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault, pixelBuffer, formatDescription, + &timingInfo, &sampleBuffer); + + CFRelease(pixelBuffer); + CFRelease(formatDescription); + return sampleBuffer; +} diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraUtilTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraUtilTests.m similarity index 98% rename from packages/camera/camera/example/ios/RunnerTests/CameraUtilTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraUtilTests.m index 380f6e93de58..d1a835c36efe 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraUtilTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraUtilTests.m @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; +@import camera_avfoundation; @import XCTest; @import AVFoundation; #import diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m similarity index 76% rename from packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m index fdb2abd4933e..8a7c34cc2731 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m @@ -2,12 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; -@import camera.Test; +@import camera_avfoundation; +@import camera_avfoundation.Test; @import AVFoundation; @import XCTest; #import +#import "CameraTestUtils.h" +/// Includes test cases related to photo capture operations for FLTCam class. @interface FLTCamPhotoCaptureTests : XCTestCase @end @@ -22,7 +24,7 @@ - (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsW dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific, (void *)FLTCaptureSessionQueueSpecific, NULL); - FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; + FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue); AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); OCMStub([mockSettings photoSettings]).andReturn(settings); @@ -61,7 +63,7 @@ - (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWi dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific, (void *)FLTCaptureSessionQueueSpecific, NULL); - FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; + FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue); AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); @@ -92,23 +94,4 @@ - (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWi [self waitForExpectationsWithTimeout:1 handler:nil]; } -/// Creates an `FLTCam` that runs its operations on a given capture session queue. -- (FLTCam *)createFLTCamWithCaptureSessionQueue:(dispatch_queue_t)captureSessionQueue { - id inputMock = OCMClassMock([AVCaptureDeviceInput class]); - OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) - .andReturn(inputMock); - - id sessionMock = OCMClassMock([AVCaptureSession class]); - OCMStub([sessionMock alloc]).andReturn(sessionMock); - OCMStub([sessionMock addInputWithNoConnections:[OCMArg any]]); // no-op - OCMStub([sessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); - - return [[FLTCam alloc] initWithCameraName:@"camera" - resolutionPreset:@"medium" - enableAudio:true - orientation:UIDeviceOrientationPortrait - captureSessionQueue:captureSessionQueue - error:nil]; -} - @end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSampleBufferTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSampleBufferTests.m new file mode 100644 index 000000000000..94426ab3aeb8 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSampleBufferTests.m @@ -0,0 +1,41 @@ +// 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 camera_avfoundation; +@import camera_avfoundation.Test; +@import AVFoundation; +@import XCTest; +#import +#import "CameraTestUtils.h" + +/// Includes test cases related to sample buffer handling for FLTCam class. +@interface FLTCamSampleBufferTests : XCTestCase + +@end + +@implementation FLTCamSampleBufferTests + +- (void)testSampleBufferCallbackQueueMustBeCaptureSessionQueue { + dispatch_queue_t captureSessionQueue = dispatch_queue_create("testing", NULL); + FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue); + XCTAssertEqual(captureSessionQueue, cam.captureVideoOutput.sampleBufferCallbackQueue, + @"Sample buffer callback queue must be the capture session queue."); +} + +- (void)testCopyPixelBuffer { + FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(dispatch_queue_create("test", NULL)); + CMSampleBufferRef capturedSampleBuffer = FLTCreateTestSampleBuffer(); + CVPixelBufferRef capturedPixelBuffer = CMSampleBufferGetImageBuffer(capturedSampleBuffer); + // Mimic sample buffer callback when captured a new video sample + [cam captureOutput:cam.captureVideoOutput + didOutputSampleBuffer:capturedSampleBuffer + fromConnection:OCMClassMock([AVCaptureConnection class])]; + CVPixelBufferRef deliveriedPixelBuffer = [cam copyPixelBuffer]; + XCTAssertEqual(deliveriedPixelBuffer, capturedPixelBuffer, + @"FLTCam must deliver the latest captured pixel buffer to copyPixelBuffer API."); + CFRelease(capturedSampleBuffer); + CFRelease(deliveriedPixelBuffer); +} + +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m similarity index 94% rename from packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m index 9e8e2441f0b9..f7633591ccb6 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; -@import camera.Test; +@import camera_avfoundation; +@import camera_avfoundation.Test; @import AVFoundation; @import XCTest; #import @@ -53,7 +53,7 @@ - (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToWrite { [completionExpectation fulfill]; }]; - // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. + // Do not use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. id mockData = OCMPartialMock([NSData data]); OCMStub([mockData writeToFile:OCMOCK_ANY @@ -82,7 +82,7 @@ - (void)testHandlePhotoCaptureResult_mustCompleteWithFilePathIfSuccessToWrite { [completionExpectation fulfill]; }]; - // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. + // Do not use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. id mockData = OCMPartialMock([NSData data]); OCMStub([mockData writeToFile:filePath options:NSDataWritingAtomic error:[OCMArg setTo:nil]]) @@ -107,7 +107,7 @@ - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue const char *ioQueueSpecific = "io_queue_specific"; dispatch_queue_set_specific(ioQueue, ioQueueSpecific, (void *)ioQueueSpecific, NULL); - // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. + // Do not use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. id mockData = OCMPartialMock([NSData data]); OCMStub([mockData writeToFile:OCMOCK_ANY options:NSDataWritingAtomic error:[OCMArg setTo:nil]]) diff --git a/packages/camera/camera/example/ios/RunnerTests/Info.plist b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/camera/camera/example/ios/RunnerTests/Info.plist rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/Info.plist diff --git a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h similarity index 100% rename from packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h diff --git a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m similarity index 95% rename from packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m index da2fc2d936ba..d3d7b6ac15b3 100644 --- a/packages/camera/camera/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; +@import camera_avfoundation; @import XCTest; #import "MockFLTThreadSafeFlutterResult.h" diff --git a/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/QueueUtilsTests.m similarity index 82% rename from packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/QueueUtilsTests.m index c5f377f7efa9..a9fc7396bb99 100644 --- a/packages/camera/camera/example/ios/RunnerTests/QueueHelperTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/QueueUtilsTests.m @@ -2,23 +2,23 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; +@import camera_avfoundation; @import XCTest; -@interface QueueHelperTests : XCTestCase +@interface QueueUtilsTests : XCTestCase @end -@implementation QueueHelperTests +@implementation QueueUtilsTests - (void)testShouldStayOnMainQueueIfCalledFromMainQueue { XCTestExpectation *expectation = [self expectationWithDescription:@"Block must be run on the main queue."]; - [QueueHelper ensureToRunOnMainQueue:^{ + FLTEnsureToRunOnMainQueue(^{ if (NSThread.isMainThread) { [expectation fulfill]; } - }]; + }); [self waitForExpectationsWithTimeout:1 handler:nil]; } @@ -26,11 +26,11 @@ - (void)testShouldDispatchToMainQueueIfCalledFromBackgroundQueue { XCTestExpectation *expectation = [self expectationWithDescription:@"Block must be run on the main queue."]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [QueueHelper ensureToRunOnMainQueue:^{ + FLTEnsureToRunOnMainQueue(^{ if (NSThread.isMainThread) { [expectation fulfill]; } - }]; + }); }); [self waitForExpectationsWithTimeout:1 handler:nil]; } diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/StreamingTest.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/StreamingTest.m new file mode 100644 index 000000000000..14a611852dcc --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/StreamingTest.m @@ -0,0 +1,85 @@ +// 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 camera_avfoundation; +@import camera_avfoundation.Test; +@import XCTest; +@import AVFoundation; +#import +#import "CameraTestUtils.h" + +@interface StreamingTests : XCTestCase +@property(readonly, nonatomic) FLTCam *camera; +@property(readonly, nonatomic) CMSampleBufferRef sampleBuffer; +@end + +@implementation StreamingTests + +- (void)setUp { + dispatch_queue_t captureSessionQueue = dispatch_queue_create("testing", NULL); + _camera = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue); + _sampleBuffer = FLTCreateTestSampleBuffer(); +} + +- (void)tearDown { + CFRelease(_sampleBuffer); +} + +- (void)testExceedMaxStreamingPendingFramesCount { + XCTestExpectation *streamingExpectation = [self + expectationWithDescription:@"Must not call handler over maxStreamingPendingFramesCount"]; + + id handlerMock = OCMClassMock([FLTImageStreamHandler class]); + OCMStub([handlerMock eventSink]).andReturn(^(id event) { + [streamingExpectation fulfill]; + }); + + id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + [_camera startImageStreamWithMessenger:messenger imageStreamHandler:handlerMock]; + + XCTKVOExpectation *expectation = [[XCTKVOExpectation alloc] initWithKeyPath:@"isStreamingImages" + object:_camera + expectedValue:@YES]; + XCTWaiterResult result = [XCTWaiter waitForExpectations:@[ expectation ] timeout:1]; + XCTAssertEqual(result, XCTWaiterResultCompleted); + + streamingExpectation.expectedFulfillmentCount = 4; + for (int i = 0; i < 10; i++) { + [_camera captureOutput:nil didOutputSampleBuffer:self.sampleBuffer fromConnection:nil]; + } + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +- (void)testReceivedImageStreamData { + XCTestExpectation *streamingExpectation = + [self expectationWithDescription: + @"Must be able to call the handler again when receivedImageStreamData is called"]; + + id handlerMock = OCMClassMock([FLTImageStreamHandler class]); + OCMStub([handlerMock eventSink]).andReturn(^(id event) { + [streamingExpectation fulfill]; + }); + + id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + [_camera startImageStreamWithMessenger:messenger imageStreamHandler:handlerMock]; + + XCTKVOExpectation *expectation = [[XCTKVOExpectation alloc] initWithKeyPath:@"isStreamingImages" + object:_camera + expectedValue:@YES]; + XCTWaiterResult result = [XCTWaiter waitForExpectations:@[ expectation ] timeout:1]; + XCTAssertEqual(result, XCTWaiterResultCompleted); + + streamingExpectation.expectedFulfillmentCount = 5; + for (int i = 0; i < 10; i++) { + [_camera captureOutput:nil didOutputSampleBuffer:self.sampleBuffer fromConnection:nil]; + } + + [_camera receivedImageStreamData]; + [_camera captureOutput:nil didOutputSampleBuffer:self.sampleBuffer fromConnection:nil]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeEventChannelTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeEventChannelTests.m similarity index 81% rename from packages/camera/camera/example/ios/RunnerTests/ThreadSafeEventChannelTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeEventChannelTests.m index dd7ca39c2e98..2aad7e3de9dd 100644 --- a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeEventChannelTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeEventChannelTests.m @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; +@import camera_avfoundation; @import XCTest; #import @@ -63,4 +63,20 @@ - (void)testSetStreamHandler_shouldDispatchToMainThreadIfCalledFromBackgroundThr [self waitForExpectationsWithTimeout:1 handler:nil]; } +- (void)testEventChannel_shouldBeKeptAliveWhenDispatchingBackToMainThread { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion should be called."]; + dispatch_async(dispatch_queue_create("test", NULL), ^{ + FLTThreadSafeEventChannel *channel = [[FLTThreadSafeEventChannel alloc] + initWithEventChannel:OCMClassMock([FlutterEventChannel class])]; + + [channel setStreamHandler:OCMOCK_ANY + completion:^{ + [expectation fulfill]; + }]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + @end diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m similarity index 99% rename from packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m index 5fdbd49e5d40..b8de19ce4ab5 100644 --- a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; +@import camera_avfoundation; @import XCTest; @interface ThreadSafeFlutterResultTests : XCTestCase diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m similarity index 98% rename from packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m index 5075be7a81e2..ce1b641cef6f 100644 --- a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; +@import camera_avfoundation; @import XCTest; #import diff --git a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m similarity index 99% rename from packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m index 067ebab3642f..31f196ffdb9e 100644 --- a/packages/camera/camera/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@import camera; +@import camera_avfoundation; @import XCTest; #import diff --git a/packages/camera/camera_avfoundation/example/lib/camera_controller.dart b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart new file mode 100644 index 000000000000..09441cc5449c --- /dev/null +++ b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart @@ -0,0 +1,437 @@ +// 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'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:quiver/core.dart'; + +/// The state of a [CameraController]. +class CameraValue { + /// Creates a new camera controller state. + const CameraValue({ + required this.isInitialized, + this.previewSize, + required this.isRecordingVideo, + required this.isTakingPicture, + required this.isStreamingImages, + required this.isRecordingPaused, + required this.flashMode, + required this.exposureMode, + required this.focusMode, + required this.deviceOrientation, + this.lockedCaptureOrientation, + this.recordingOrientation, + this.isPreviewPaused = false, + this.previewPauseOrientation, + }); + + /// 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, + focusMode: FocusMode.auto, + 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; + + /// True when video recording is paused. + 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; + + /// The size of the preview in pixels. + /// + /// Is `null` until [isInitialized] is `true`. + final Size? previewSize; + + /// 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; + + /// 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, + 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, + previewSize: previewSize ?? this.previewSize, + isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo, + isTakingPicture: isTakingPicture ?? this.isTakingPicture, + isStreamingImages: isStreamingImages ?? this.isStreamingImages, + isRecordingPaused: isRecordingPaused ?? this.isRecordingPaused, + flashMode: flashMode ?? this.flashMode, + exposureMode: exposureMode ?? this.exposureMode, + focusMode: focusMode ?? this.focusMode, + 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, ' + 'previewSize: $previewSize, ' + 'isStreamingImages: $isStreamingImages, ' + 'flashMode: $flashMode, ' + 'exposureMode: $exposureMode, ' + 'focusMode: $focusMode, ' + 'deviceOrientation: $deviceOrientation, ' + 'lockedCaptureOrientation: $lockedCaptureOrientation, ' + 'recordingOrientation: $recordingOrientation, ' + 'isPreviewPaused: $isPreviewPaused, ' + 'previewPausedOrientation: $previewPauseOrientation)'; + } +} + +/// Controls a device camera. +/// +/// This is a stripped-down version of the app-facing controller to serve as a +/// utility for the example and integration tests. It wraps only the calls that +/// have state associated with them, to consolidate tracking of camera state +/// outside of the overall example code. +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; + + late int _cameraId; + + bool _isDisposed = false; + StreamSubscription? _imageStreamSubscription; + FutureOr? _initCalled; + StreamSubscription? + _deviceOrientationSubscription; + + /// The camera identifier with which the controller is associated. + int get cameraId => _cameraId; + + /// Initializes the camera on the device. + Future initialize() async { + 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, + ); + + 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), + ); + + _initCalled = true; + } + + /// Prepare the capture session for video recording. + Future prepareForVideoRecording() async { + await CameraPlatform.instance.prepareForVideoRecording(); + } + + /// Pauses the current camera preview + Future pausePreview() async { + await CameraPlatform.instance.pausePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: true, + 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, + previewPauseOrientation: const Optional.absent()); + } + + /// Captures an image and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture fails. + Future takePicture() async { + value = value.copyWith(isTakingPicture: true); + final XFile file = await CameraPlatform.instance.takePicture(_cameraId); + value = value.copyWith(isTakingPicture: false); + return file; + } + + /// Start streaming images from platform camera. + Future startImageStream( + Function(CameraImageData image) onAvailable) async { + _imageStreamSubscription = CameraPlatform.instance + .onStreamedFrameAvailable(_cameraId) + .listen((CameraImageData imageData) { + onAvailable(imageData); + }); + value = value.copyWith(isStreamingImages: true); + } + + /// Stop streaming images from platform camera. + Future stopImageStream() async { + value = value.copyWith(isStreamingImages: false); + await _imageStreamSubscription?.cancel(); + _imageStreamSubscription = null; + } + + /// Start a video recording. + /// + /// The video is returned as a [XFile] after calling [stopVideoRecording]. + /// Throws a [CameraException] if the capture fails. + Future startVideoRecording() async { + await CameraPlatform.instance.startVideoRecording(_cameraId); + value = value.copyWith( + isRecordingVideo: true, + isRecordingPaused: false, + recordingOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); + } + + /// Stops the video recording and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture failed. + Future stopVideoRecording() async { + final XFile file = + await CameraPlatform.instance.stopVideoRecording(_cameraId); + value = value.copyWith( + isRecordingVideo: false, + recordingOrientation: const Optional.absent(), + ); + return file; + } + + /// Pause video recording. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future pauseVideoRecording() async { + await CameraPlatform.instance.pauseVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: true); + } + + /// Resume video recording after pausing. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future resumeVideoRecording() async { + await CameraPlatform.instance.resumeVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: false); + } + + /// Returns a widget showing a live camera preview. + Widget buildPreview() { + return CameraPlatform.instance.buildPreview(_cameraId); + } + + /// Sets the flash mode for taking pictures. + Future setFlashMode(FlashMode mode) async { + await CameraPlatform.instance.setFlashMode(_cameraId, mode); + value = value.copyWith(flashMode: mode); + } + + /// Sets the exposure mode for taking pictures. + Future setExposureMode(ExposureMode mode) async { + await CameraPlatform.instance.setExposureMode(_cameraId, mode); + value = value.copyWith(exposureMode: mode); + } + + /// Sets the exposure offset for the selected camera. + Future setExposureOffset(double offset) async { + // Check if offset is in range + final List range = await Future.wait(>[ + CameraPlatform.instance.getMinExposureOffset(_cameraId), + CameraPlatform.instance.getMaxExposureOffset(_cameraId) + ]); + + // Round to the closest step if needed + final double stepSize = + await CameraPlatform.instance.getExposureOffsetStepSize(_cameraId); + 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; + } + + return CameraPlatform.instance.setExposureOffset(_cameraId, offset); + } + + /// Locks the capture orientation. + /// + /// If [orientation] is omitted, the current device orientation is used. + Future lockCaptureOrientation() async { + await CameraPlatform.instance + .lockCaptureOrientation(_cameraId, 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. + Future setFocusMode(FocusMode mode) async { + await CameraPlatform.instance.setFocusMode(_cameraId, mode); + value = value.copyWith(focusMode: mode); + } + + /// Releases the resources of this camera. + @override + Future dispose() async { + if (_isDisposed) { + return; + } + _deviceOrientationSubscription?.cancel(); + _isDisposed = true; + super.dispose(); + if (_initCalled != null) { + await _initCalled; + await CameraPlatform.instance.dispose(_cameraId); + } + } + + @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); + } + } +} diff --git a/packages/camera/camera_avfoundation/example/lib/camera_preview.dart b/packages/camera/camera_avfoundation/example/lib/camera_preview.dart new file mode 100644 index 000000000000..5e8f64cb2fbd --- /dev/null +++ b/packages/camera/camera_avfoundation/example/lib/camera_preview.dart @@ -0,0 +1,85 @@ +// 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, {Key? key, this.child}) + : super(key: key); + + /// 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) { + final double cameraAspectRatio = + controller.value.previewSize!.width / + controller.value.previewSize!.height; + return AspectRatio( + aspectRatio: _isLandscape() + ? cameraAspectRatio + : (1 / cameraAspectRatio), + 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_avfoundation/example/lib/main.dart b/packages/camera/camera_avfoundation/example/lib/main.dart new file mode 100644 index 000000000000..9ebc27e4be5b --- /dev/null +++ b/packages/camera/camera_avfoundation/example/lib/main.dart @@ -0,0 +1,1096 @@ +// 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'; +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/scheduler.dart'; +import 'package:video_player/video_player.dart'; + +import 'camera_controller.dart'; +import 'camera_preview.dart'; + +/// Camera example home widget. +class CameraExampleHome extends StatefulWidget { + /// Default Constructor + const CameraExampleHome({Key? key}) : super(key: key); + + @override + State createState() { + return _CameraExampleHomeState(); + } +} + +/// 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; + default: + throw ArgumentError('Unknown lens direction'); + } +} + +void _logError(String code, String? message) { + if (message != null) { + print('Error: $code\nError Message: $message'); + } else { + print('Error: $code'); + } +} + +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(); + _ambiguate(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() { + _ambiguate(WidgetsBinding.instance)?.removeObserver(this); + _flashModeControlRowAnimationController.dispose(); + _exposureModeControlRowAnimationController.dispose(); + super.dispose(); + } + + @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); + } + } + + @override + Widget build(BuildContext context) { + 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; + } + + _currentScale = (_baseScale * details.scale) + .clamp(_minAvailableZoom, _maxAvailableZoom); + + await CameraPlatform.instance + .setZoomLevel(controller!.cameraId, _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: controller != null ? onFlashModeButtonPressed : null, + ), + // The exposure and focus mode are currently not supported on the web. + ...!kIsWeb + ? [ + IconButton( + icon: const Icon(Icons.exposure), + color: Colors.blue, + onPressed: controller != null + ? onExposureModeButtonPressed + : null, + ), + IconButton( + icon: const Icon(Icons.filter_center_focus), + color: Colors.blue, + onPressed: + controller != null ? onFocusModeButtonPressed : null, + ) + ] + : [], + IconButton( + icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), + color: Colors.blue, + onPressed: controller != null ? onAudioModeButtonPressed : null, + ), + IconButton( + icon: Icon(controller?.value.isCaptureOrientationLocked ?? false + ? Icons.screen_lock_rotation + : Icons.screen_rotation), + color: Colors.blue, + onPressed: controller != null + ? onCaptureOrientationLockButtonPressed + : null, + ), + ], + ), + _flashModeControlRowWidget(), + _exposureModeControlRowWidget(), + _focusModeControlRowWidget(), + ], + ); + } + + Widget _flashModeControlRowWidget() { + return SizeTransition( + sizeFactor: _flashModeControlRowAnimation, + child: ClipRect( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon(Icons.flash_off), + color: controller?.value.flashMode == FlashMode.off + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.off) + : null, + ), + IconButton( + icon: const Icon(Icons.flash_auto), + color: controller?.value.flashMode == FlashMode.auto + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.auto) + : null, + ), + IconButton( + icon: const Icon(Icons.flash_on), + color: controller?.value.flashMode == FlashMode.always + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.always) + : null, + ), + IconButton( + icon: const Icon(Icons.highlight), + color: controller?.value.flashMode == FlashMode.torch + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.torch) + : null, + ), + ], + ), + ), + ); + } + + Widget _exposureModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + // 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: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.auto) + : null, + onLongPress: () { + if (controller != null) { + CameraPlatform.instance + .setExposurePoint(controller!.cameraId, null); + showInSnackBar('Resetting exposure point'); + } + }, + child: const Text('AUTO'), + ), + TextButton( + style: styleLocked, + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.locked) + : null, + child: const Text('LOCKED'), + ), + TextButton( + style: styleLocked, + onPressed: controller != null + ? () => controller!.setExposureOffset(0.0) + : null, + 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: controller != null + ? () => onSetFocusModeButtonPressed(FocusMode.auto) + : null, + onLongPress: () { + if (controller != null) { + CameraPlatform.instance + .setFocusPoint(controller!.cameraId, null); + } + showInSnackBar('Resetting focus point'); + }, + child: const Text('AUTO'), + ), + TextButton( + style: styleLocked, + onPressed: controller != null + ? () => onSetFocusModeButtonPressed(FocusMode.locked) + : null, + 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: cameraController != null && + cameraController.value.isInitialized && + !cameraController.value.isRecordingVideo + ? onTakePictureButtonPressed + : null, + ), + IconButton( + icon: const Icon(Icons.videocam), + color: Colors.blue, + onPressed: cameraController != null && + cameraController.value.isInitialized && + !cameraController.value.isRecordingVideo + ? onVideoRecordButtonPressed + : null, + ), + IconButton( + icon: cameraController != null && + (!cameraController.value.isRecordingVideo || + cameraController.value.isRecordingPaused) + ? const Icon(Icons.play_arrow) + : const Icon(Icons.pause), + color: Colors.blue, + onPressed: cameraController != null && + cameraController.value.isInitialized && + cameraController.value.isRecordingVideo + ? (cameraController.value.isRecordingPaused) + ? onResumeButtonPressed + : onPauseButtonPressed + : null, + ), + IconButton( + icon: const Icon(Icons.stop), + color: Colors.red, + onPressed: cameraController != null && + cameraController.value.isInitialized && + cameraController.value.isRecordingVideo + ? onStopButtonPressed + : null, + ), + 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) { + _ambiguate(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 Point point = Point( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ); + CameraPlatform.instance.setExposurePoint(cameraController.cameraId, point); + CameraPlatform.instance.setFocusPoint(cameraController.cameraId, point); + } + + 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(() {}); + } + }); + + try { + await cameraController.initialize(); + await Future.wait(>[ + // The exposure mode is currently not supported on the web. + ...!kIsWeb + ? >[ + CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId) + .then( + (double value) => _minAvailableExposureOffset = value), + CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId) + .then((double value) => _maxAvailableExposureOffset = value) + ] + : >[], + CameraPlatform.instance + .getMaxZoomLevel(cameraController.cameraId) + .then((double value) => _maxAvailableZoom = value), + CameraPlatform.instance + .getMinZoomLevel(cameraController.cameraId) + .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; + case 'cameraPermission': + // Android & web only + showInSnackBar('Unknown permission error.'); + 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({Key? key}) : super(key: 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 CameraPlatform.instance.availableCameras(); + } on CameraException catch (e) { + _logError(e.code, e.description); + } + runApp(const CameraApp()); +} + +/// 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. +// 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 new file mode 100644 index 000000000000..a9252cbd6d61 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/pubspec.yaml @@ -0,0 +1,34 @@ +name: camera_example +description: Demonstrates how to use the camera plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + camera_avfoundation: + # When depending on this package from a real application you should use: + # camera_avfoundation: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + camera_platform_interface: ^2.2.0 + flutter: + sdk: flutter + path_provider: ^2.0.0 + quiver: ^3.0.0 + video_player: ^2.1.4 + +dev_dependencies: + build_runner: ^2.1.10 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/camera/camera_avfoundation/example/test_driver/integration_test.dart b/packages/camera/camera_avfoundation/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/test_driver/integration_test.dart @@ -0,0 +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. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/camera/camera/ios/Assets/.gitkeep b/packages/camera/camera_avfoundation/ios/Assets/.gitkeep similarity index 100% rename from packages/camera/camera/ios/Assets/.gitkeep rename to packages/camera/camera_avfoundation/ios/Assets/.gitkeep diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraPermissionUtils.h b/packages/camera/camera_avfoundation/ios/Classes/CameraPermissionUtils.h new file mode 100644 index 000000000000..5cbbab055f34 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPermissionUtils.h @@ -0,0 +1,32 @@ +// 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 Foundation; +#import + +typedef void (^FLTCameraPermissionRequestCompletionHandler)(FlutterError *); + +/// Requests camera access permission. +/// +/// If it is the first time requesting camera access, a permission dialog will show up on the +/// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the +/// user will have to update the choice in Settings app. +/// +/// @param handler if access permission is (or was previously) granted, completion handler will be +/// called without error; Otherwise completion handler will be called with error. Handler can be +/// called on an arbitrary dispatch queue. +extern void FLTRequestCameraPermissionWithCompletionHandler( + FLTCameraPermissionRequestCompletionHandler handler); + +/// Requests audio access permission. +/// +/// If it is the first time requesting audio access, a permission dialog will show up on the +/// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the +/// user will have to update the choice in Settings app. +/// +/// @param handler if access permission is (or was previously) granted, completion handler will be +/// called without error; Otherwise completion handler will be called with error. Handler can be +/// called on an arbitrary dispatch queue. +extern void FLTRequestAudioPermissionWithCompletionHandler( + FLTCameraPermissionRequestCompletionHandler handler); diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraPermissionUtils.m b/packages/camera/camera_avfoundation/ios/Classes/CameraPermissionUtils.m new file mode 100644 index 000000000000..098265a6b74d --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPermissionUtils.m @@ -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 AVFoundation; +#import "CameraPermissionUtils.h" + +void FLTRequestPermission(BOOL forAudio, FLTCameraPermissionRequestCompletionHandler handler) { + AVMediaType mediaType; + if (forAudio) { + mediaType = AVMediaTypeAudio; + } else { + mediaType = AVMediaTypeVideo; + } + + switch ([AVCaptureDevice authorizationStatusForMediaType:mediaType]) { + case AVAuthorizationStatusAuthorized: + handler(nil); + break; + case AVAuthorizationStatusDenied: { + FlutterError *flutterError; + if (forAudio) { + flutterError = + [FlutterError errorWithCode:@"AudioAccessDeniedWithoutPrompt" + message:@"User has previously denied the audio access request. " + @"Go to Settings to enable audio access." + details:nil]; + } else { + flutterError = + [FlutterError errorWithCode:@"CameraAccessDeniedWithoutPrompt" + message:@"User has previously denied the camera access request. " + @"Go to Settings to enable camera access." + details:nil]; + } + handler(flutterError); + break; + } + case AVAuthorizationStatusRestricted: { + FlutterError *flutterError; + if (forAudio) { + flutterError = [FlutterError errorWithCode:@"AudioAccessRestricted" + message:@"Audio access is restricted. " + details:nil]; + } else { + flutterError = [FlutterError errorWithCode:@"CameraAccessRestricted" + message:@"Camera access is restricted. " + details:nil]; + } + handler(flutterError); + break; + } + case AVAuthorizationStatusNotDetermined: { + [AVCaptureDevice requestAccessForMediaType:mediaType + completionHandler:^(BOOL granted) { + // handler can be invoked on an arbitrary dispatch queue. + if (granted) { + handler(nil); + } else { + FlutterError *flutterError; + if (forAudio) { + flutterError = [FlutterError + errorWithCode:@"AudioAccessDenied" + message:@"User denied the audio access request." + details:nil]; + } else { + flutterError = [FlutterError + errorWithCode:@"CameraAccessDenied" + message:@"User denied the camera access request." + details:nil]; + } + handler(flutterError); + } + }]; + break; + } + } +} + +void FLTRequestCameraPermissionWithCompletionHandler( + FLTCameraPermissionRequestCompletionHandler handler) { + FLTRequestPermission(/*forAudio*/ NO, handler); +} + +void FLTRequestAudioPermissionWithCompletionHandler( + FLTCameraPermissionRequestCompletionHandler handler) { + FLTRequestPermission(/*forAudio*/ YES, handler); +} diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.h b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.h similarity index 100% rename from packages/camera/camera/ios/Classes/CameraPlugin.h rename to packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.h diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m similarity index 73% rename from packages/camera/camera/ios/Classes/CameraPlugin.m rename to packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m index fcea190de705..628211ac7f7a 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m @@ -7,25 +7,25 @@ @import AVFoundation; +#import "CameraPermissionUtils.h" #import "CameraProperties.h" #import "FLTCam.h" #import "FLTThreadSafeEventChannel.h" #import "FLTThreadSafeFlutterResult.h" #import "FLTThreadSafeMethodChannel.h" #import "FLTThreadSafeTextureRegistry.h" -#import "QueueHelper.h" +#import "QueueUtils.h" @interface CameraPlugin () @property(readonly, nonatomic) FLTThreadSafeTextureRegistry *registry; @property(readonly, nonatomic) NSObject *messenger; -@property(readonly, nonatomic) FLTThreadSafeMethodChannel *deviceEventMethodChannel; @end @implementation CameraPlugin + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/camera" + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/camera_avfoundation" binaryMessenger:[registrar messenger]]; CameraPlugin *instance = [[CameraPlugin alloc] initWithRegistry:[registrar textures] messenger:[registrar messenger]]; @@ -48,13 +48,17 @@ - (instancetype)initWithRegistry:(NSObject *)registry } - (void)initDeviceEventMethodChannel { - FlutterMethodChannel *methodChannel = - [FlutterMethodChannel methodChannelWithName:@"flutter.io/cameraPlugin/device" - binaryMessenger:_messenger]; + FlutterMethodChannel *methodChannel = [FlutterMethodChannel + methodChannelWithName:@"plugins.flutter.io/camera_avfoundation/fromPlatform" + binaryMessenger:_messenger]; _deviceEventMethodChannel = [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel]; } +- (void)detachFromEngineForRegistrar:(NSObject *)registrar { + [UIDevice.currentDevice endGeneratingDeviceOrientationNotifications]; +} + - (void)startOrientationListener { [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; [[NSNotificationCenter defaultCenter] addObserver:self @@ -72,11 +76,12 @@ - (void)orientationChanged:(NSNotification *)note { return; } + __weak typeof(self) weakSelf = self; dispatch_async(self.captureSessionQueue, ^{ // `FLTCam::setDeviceOrientation` must be called on capture session queue. - [self.camera setDeviceOrientation:orientation]; + [weakSelf.camera setDeviceOrientation:orientation]; // `CameraPlugin::sendDeviceOrientation` can be called on any queue. - [self sendDeviceOrientation:orientation]; + [weakSelf sendDeviceOrientation:orientation]; }); } @@ -88,11 +93,11 @@ - (void)sendDeviceOrientation:(UIDeviceOrientation)orientation { - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { // Invoke the plugin on another dispatch queue to avoid blocking the UI. - dispatch_async(_captureSessionQueue, ^{ + __weak typeof(self) weakSelf = self; + dispatch_async(self.captureSessionQueue, ^{ FLTThreadSafeFlutterResult *threadSafeResult = [[FLTThreadSafeFlutterResult alloc] initWithResult:result]; - - [self handleMethodCallAsync:call result:threadSafeResult]; + [weakSelf handleMethodCallAsync:call result:threadSafeResult]; }); } @@ -100,8 +105,14 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FLTThreadSafeFlutterResult *)result { if ([@"availableCameras" isEqualToString:call.method]) { if (@available(iOS 10.0, *)) { + NSMutableArray *discoveryDevices = + [@[ AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeBuiltInTelephotoCamera ] + mutableCopy]; + if (@available(iOS 13.0, *)) { + [discoveryDevices addObject:AVCaptureDeviceTypeBuiltInUltraWideCamera]; + } AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession - discoverySessionWithDeviceTypes:@[ AVCaptureDeviceTypeBuiltInWideAngleCamera ] + discoverySessionWithDeviceTypes:discoveryDevices mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionUnspecified]; NSArray *devices = discoverySession.devices; @@ -131,37 +142,16 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [result sendNotImplemented]; } } else if ([@"create" isEqualToString:call.method]) { - NSString *cameraName = call.arguments[@"cameraName"]; - NSString *resolutionPreset = call.arguments[@"resolutionPreset"]; - NSNumber *enableAudio = call.arguments[@"enableAudio"]; - NSError *error; - FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName - resolutionPreset:resolutionPreset - enableAudio:[enableAudio boolValue] - orientation:[[UIDevice currentDevice] orientation] - captureSessionQueue:_captureSessionQueue - error:&error]; - - if (error) { - [result sendError:error]; - } else { - if (_camera) { - [_camera close]; - } - _camera = cam; - [self.registry registerTexture:cam - completion:^(int64_t textureId) { - [result sendSuccessWithData:@{ - @"cameraId" : @(textureId), - }]; - }]; - } + [self handleCreateMethodCall:call result:result]; } else if ([@"startImageStream" isEqualToString:call.method]) { [_camera startImageStreamWithMessenger:_messenger]; [result sendSuccess]; } else if ([@"stopImageStream" isEqualToString:call.method]) { [_camera stopImageStream]; [result sendSuccess]; + } else if ([@"receivedImageStreamData" isEqualToString:call.method]) { + [_camera receivedImageStreamData]; + [result sendSuccess]; } else { NSDictionary *argsMap = call.arguments; NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue; @@ -176,8 +166,9 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call } }; FlutterMethodChannel *methodChannel = [FlutterMethodChannel - methodChannelWithName:[NSString stringWithFormat:@"flutter.io/cameraPlugin/camera%lu", - (unsigned long)cameraId] + methodChannelWithName: + [NSString stringWithFormat:@"plugins.flutter.io/camera_avfoundation/camera%lu", + (unsigned long)cameraId] binaryMessenger:_messenger]; FLTThreadSafeMethodChannel *threadSafeMethodChannel = [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel]; @@ -207,7 +198,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [_camera close]; [result sendSuccess]; } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { - [_camera setUpCaptureSessionForAudio]; + [self.camera setUpCaptureSessionForAudio]; [result sendSuccess]; } else if ([@"startVideoRecording" isEqualToString:call.method]) { [_camera startVideoRecordingWithResult:result]; @@ -271,4 +262,73 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call } } +- (void)handleCreateMethodCall:(FlutterMethodCall *)call + result:(FLTThreadSafeFlutterResult *)result { + // Create FLTCam only if granted camera access (and audio access if audio is enabled) + __weak typeof(self) weakSelf = self; + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + typeof(self) strongSelf = weakSelf; + if (!strongSelf) return; + + if (error) { + [result sendFlutterError:error]; + } else { + // Request audio permission on `create` call with `enableAudio` argument instead of the + // `prepareForVideoRecording` call. This is because `prepareForVideoRecording` call is + // optional, and used as a workaround to fix a missing frame issue on iOS. + BOOL audioEnabled = [call.arguments[@"enableAudio"] boolValue]; + if (audioEnabled) { + // Setup audio capture session only if granted audio access. + FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { + // cannot use the outter `strongSelf` + typeof(self) strongSelf = weakSelf; + if (!strongSelf) return; + if (error) { + [result sendFlutterError:error]; + } else { + [strongSelf createCameraOnSessionQueueWithCreateMethodCall:call result:result]; + } + }); + } else { + [strongSelf createCameraOnSessionQueueWithCreateMethodCall:call result:result]; + } + } + }); +} + +- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall + result:(FLTThreadSafeFlutterResult *)result { + __weak typeof(self) weakSelf = self; + dispatch_async(self.captureSessionQueue, ^{ + typeof(self) strongSelf = weakSelf; + if (!strongSelf) return; + + NSString *cameraName = createMethodCall.arguments[@"cameraName"]; + NSString *resolutionPreset = createMethodCall.arguments[@"resolutionPreset"]; + NSNumber *enableAudio = createMethodCall.arguments[@"enableAudio"]; + NSError *error; + FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName + resolutionPreset:resolutionPreset + enableAudio:[enableAudio boolValue] + orientation:[[UIDevice currentDevice] orientation] + captureSessionQueue:strongSelf.captureSessionQueue + error:&error]; + + if (error) { + [result sendError:error]; + } else { + if (strongSelf.camera) { + [strongSelf.camera close]; + } + strongSelf.camera = cam; + [strongSelf.registry registerTexture:cam + completion:^(int64_t textureId) { + [result sendSuccessWithData:@{ + @"cameraId" : @(textureId), + }]; + }]; + } + }); +} + @end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.modulemap similarity index 71% rename from packages/camera/camera/ios/Classes/CameraPlugin.modulemap rename to packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.modulemap index 529c6580e908..abdad1ab575c 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.modulemap @@ -1,11 +1,12 @@ -framework module camera { - umbrella header "camera-umbrella.h" +framework module camera_avfoundation { + umbrella header "camera_avfoundation-umbrella.h" export * module * { export * } explicit module Test { header "CameraPlugin_Test.h" + header "CameraPermissionUtils.h" header "CameraProperties.h" header "FLTCam.h" header "FLTCam_Test.h" @@ -14,6 +15,6 @@ framework module camera { header "FLTThreadSafeFlutterResult.h" header "FLTThreadSafeMethodChannel.h" header "FLTThreadSafeTextureRegistry.h" - header "QueueHelper.h" + header "QueueUtils.h" } } diff --git a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin_Test.h similarity index 68% rename from packages/camera/camera/ios/Classes/CameraPlugin_Test.h rename to packages/camera/camera_avfoundation/ios/Classes/CameraPlugin_Test.h index 826b05043f78..f6c97da4ad84 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin_Test.h @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// This header is available in the Test module. Import via "@import camera.Test;" +// This header is available in the Test module. Import via "@import camera_avfoundation.Test;" -#import -#import -#import +#import "CameraPlugin.h" +#import "FLTCam.h" +#import "FLTThreadSafeFlutterResult.h" -/// Methods exposed for unit testing. +/// APIs exposed for unit testing. @interface CameraPlugin () /// All FLTCam's state access and capture session related operations should be on run on this queue. @@ -17,6 +17,10 @@ /// An internal camera object that manages camera's state and performs camera operations. @property(nonatomic, strong) FLTCam *camera; +/// A thread safe wrapper of the method channel used to send device events such as orientation +/// changes. +@property(nonatomic, strong) FLTThreadSafeMethodChannel *deviceEventMethodChannel; + /// Inject @p FlutterTextureRegistry and @p FlutterBinaryMessenger for unit testing. - (instancetype)initWithRegistry:(NSObject *)registry messenger:(NSObject *)messenger @@ -38,4 +42,10 @@ /// that triggered the orientation change. - (void)orientationChanged:(NSNotification *)notification; +/// Creates FLTCam on session queue and reports the creation result. +/// @param createMethodCall the create method call +/// @param result a thread safe flutter result wrapper object to report creation result. +- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall + result:(FLTThreadSafeFlutterResult *)result; + @end diff --git a/packages/camera/camera/ios/Classes/CameraProperties.h b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.h similarity index 100% rename from packages/camera/camera/ios/Classes/CameraProperties.h rename to packages/camera/camera_avfoundation/ios/Classes/CameraProperties.h diff --git a/packages/camera/camera/ios/Classes/CameraProperties.m b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m similarity index 100% rename from packages/camera/camera/ios/Classes/CameraProperties.m rename to packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m diff --git a/packages/camera/camera/ios/Classes/FLTCam.h b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h similarity index 85% rename from packages/camera/camera/ios/Classes/FLTCam.h rename to packages/camera/camera_avfoundation/ios/Classes/FLTCam.h index 417a1d74db21..8a5dafaf8354 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.h +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h @@ -31,6 +31,13 @@ NS_ASSUME_NONNULL_BEGIN // Format used for video and image streaming. @property(assign, nonatomic) FourCharCode videoFormat; +/// Initializes an `FLTCam` instance. +/// @param cameraName a name used to uniquely identify the camera. +/// @param resolutionPreset the resolution preset +/// @param enableAudio YES if audio should be enabled for video capturing; NO otherwise. +/// @param orientation the orientation of camera +/// @param captureSessionQueue the queue on which camera's capture session operations happen. +/// @param error report to the caller if any error happened creating the camera. - (instancetype)initWithCameraName:(NSString *)cameraName resolutionPreset:(NSString *)resolutionPreset enableAudio:(BOOL)enableAudio @@ -54,6 +61,14 @@ NS_ASSUME_NONNULL_BEGIN - (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr; - (void)applyFocusMode; +/** + * Acknowledges the receipt of one image stream frame. + * + * This should be called each time a frame is received. Failing to call it may + * cause later frames to be dropped instead of streamed. + */ +- (void)receivedImageStreamData; + /** * Applies FocusMode on the AVCaptureDevice. * diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m similarity index 88% rename from packages/camera/camera/ios/Classes/FLTCam.m rename to packages/camera/camera_avfoundation/ios/Classes/FLTCam.m index 31a9decd59b9..90b81adbd84c 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m @@ -5,19 +5,11 @@ #import "FLTCam.h" #import "FLTCam_Test.h" #import "FLTSavePhotoDelegate.h" -#import "QueueHelper.h" +#import "QueueUtils.h" @import CoreMotion; #import -@interface FLTImageStreamHandler : NSObject -// The queue on which `eventSink` property should be accessed -@property(nonatomic, strong) dispatch_queue_t captureSessionQueue; -// `eventSink` property should be accessed on `captureSessionQueue`. -// The block itself should be invoked on the main queue. -@property FlutterEventSink eventSink; -@end - @implementation FLTImageStreamHandler - (instancetype)initWithCaptureSessionQueue:(dispatch_queue_t)captureSessionQueue { @@ -28,16 +20,18 @@ - (instancetype)initWithCaptureSessionQueue:(dispatch_queue_t)captureSessionQueu } - (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { + __weak typeof(self) weakSelf = self; dispatch_async(self.captureSessionQueue, ^{ - self.eventSink = nil; + weakSelf.eventSink = nil; }); return nil; } - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(nonnull FlutterEventSink)events { + __weak typeof(self) weakSelf = self; dispatch_async(self.captureSessionQueue, ^{ - self.eventSink = events; + weakSelf.eventSink = events; }); return nil; } @@ -52,7 +46,9 @@ @interface FLTCam () *)messenger { + [self startImageStreamWithMessenger:messenger + imageStreamHandler:[[FLTImageStreamHandler alloc] + initWithCaptureSessionQueue:_captureSessionQueue]]; +} + +- (void)startImageStreamWithMessenger:(NSObject *)messenger + imageStreamHandler:(FLTImageStreamHandler *)imageStreamHandler { if (!_isStreamingImages) { - FlutterEventChannel *eventChannel = - [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/camera/imageStream" - binaryMessenger:messenger]; + FlutterEventChannel *eventChannel = [FlutterEventChannel + eventChannelWithName:@"plugins.flutter.io/camera_avfoundation/imageStream" + binaryMessenger:messenger]; FLTThreadSafeEventChannel *threadSafeEventChannel = [[FLTThreadSafeEventChannel alloc] initWithEventChannel:eventChannel]; - _imageStreamHandler = - [[FLTImageStreamHandler alloc] initWithCaptureSessionQueue:_captureSessionQueue]; + _imageStreamHandler = imageStreamHandler; + __weak typeof(self) weakSelf = self; [threadSafeEventChannel setStreamHandler:_imageStreamHandler completion:^{ - dispatch_async(self->_captureSessionQueue, ^{ - self.isStreamingImages = YES; + typeof(self) strongSelf = weakSelf; + if (!strongSelf) return; + + dispatch_async(strongSelf.captureSessionQueue, ^{ + // cannot use the outter strongSelf + typeof(self) strongSelf = weakSelf; + if (!strongSelf) return; + + strongSelf.isStreamingImages = YES; + strongSelf.streamingPendingFramesCount = 0; }); }]; } else { @@ -899,6 +953,10 @@ - (void)stopImageStream { } } +- (void)receivedImageStreamData { + self.streamingPendingFramesCount--; +} + - (void)getMaxZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result { CGFloat maxZoomFactor = [self getMaxAvailableZoomFactor]; diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam_Test.h b/packages/camera/camera_avfoundation/ios/Classes/FLTCam_Test.h new file mode 100644 index 000000000000..19e284227f4f --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam_Test.h @@ -0,0 +1,61 @@ +// 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 "FLTCam.h" +#import "FLTSavePhotoDelegate.h" + +@interface FLTImageStreamHandler : NSObject + +/// The queue on which `eventSink` property should be accessed. +@property(nonatomic, strong) dispatch_queue_t captureSessionQueue; + +/// The event sink to stream camera events to Dart. +/// +/// The property should only be accessed on `captureSessionQueue`. +/// The block itself should be invoked on the main queue. +@property FlutterEventSink eventSink; + +@end + +// APIs exposed for unit testing. +@interface FLTCam () + +/// The output for video capturing. +@property(readonly, nonatomic) AVCaptureVideoDataOutput *captureVideoOutput; + +/// The output for photo capturing. Exposed setter for unit tests. +@property(strong, nonatomic) AVCapturePhotoOutput *capturePhotoOutput API_AVAILABLE(ios(10)); + +/// True when images from the camera are being streamed. +@property(assign, nonatomic) BOOL isStreamingImages; + +/// A dictionary to retain all in-progress FLTSavePhotoDelegates. The key of the dictionary is the +/// AVCapturePhotoSettings's uniqueID for each photo capture operation, and the value is the +/// FLTSavePhotoDelegate that handles the result of each photo capture operation. Note that photo +/// capture operations may overlap, so FLTCam has to keep track of multiple delegates in progress, +/// instead of just a single delegate reference. +@property(readonly, nonatomic) + NSMutableDictionary *inProgressSavePhotoDelegates; + +/// Delegate callback when receiving a new video or audio sample. +/// Exposed for unit tests. +- (void)captureOutput:(AVCaptureOutput *)output + didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + fromConnection:(AVCaptureConnection *)connection; + +/// Initializes a camera instance. +/// Allows for injecting dependencies that are usually internal. +- (instancetype)initWithCameraName:(NSString *)cameraName + resolutionPreset:(NSString *)resolutionPreset + enableAudio:(BOOL)enableAudio + orientation:(UIDeviceOrientation)orientation + captureSession:(AVCaptureSession *)captureSession + captureSessionQueue:(dispatch_queue_t)captureSessionQueue + error:(NSError **)error; + +/// Start streaming images. +- (void)startImageStreamWithMessenger:(NSObject *)messenger + imageStreamHandler:(FLTImageStreamHandler *)imageStreamHandler; + +@end diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h b/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.h similarity index 100% rename from packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h rename to packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.h diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m b/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.m similarity index 83% rename from packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m rename to packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.m index ced3cb5e407f..617890c44055 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.m @@ -31,17 +31,24 @@ - (void)handlePhotoCaptureResultWithError:(NSError *)error self.completionHandler(nil, error); return; } + __weak typeof(self) weakSelf = self; dispatch_async(self.ioQueue, ^{ + typeof(self) strongSelf = weakSelf; + if (!strongSelf) return; + NSData *data = photoDataProvider(); NSError *ioError; - if ([data writeToFile:self.path options:NSDataWritingAtomic error:&ioError]) { - self.completionHandler(self.path, nil); + if ([data writeToFile:strongSelf.path options:NSDataWritingAtomic error:&ioError]) { + strongSelf.completionHandler(self.path, nil); } else { - self.completionHandler(nil, ioError); + strongSelf.completionHandler(nil, ioError); } }); } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wdeprecated-implementations" - (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer @@ -56,6 +63,7 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output previewPhotoSampleBuffer]; }]; } +#pragma clang diagnostic pop - (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h b/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate_Test.h similarity index 100% rename from packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h rename to packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate_Test.h diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.h similarity index 100% rename from packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.h rename to packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.h diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.m similarity index 58% rename from packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m rename to packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.m index 02a36f152bd8..57d154c595ec 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeEventChannel.m +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.m @@ -3,7 +3,7 @@ // found in the LICENSE file. #import "FLTThreadSafeEventChannel.h" -#import "QueueHelper.h" +#import "QueueUtils.h" @interface FLTThreadSafeEventChannel () @property(nonatomic, strong) FlutterEventChannel *channel; @@ -21,10 +21,15 @@ - (instancetype)initWithEventChannel:(FlutterEventChannel *)channel { - (void)setStreamHandler:(NSObject *)handler completion:(void (^)(void))completion { - [QueueHelper ensureToRunOnMainQueue:^{ + // WARNING: Should not use weak self, because FLTThreadSafeEventChannel is a local variable + // (retained within call stack, but not in the heap). FLTEnsureToRunOnMainQueue may trigger a + // context switch (when calling from background thread), in which case using weak self will always + // result in a nil self. Alternative to using strong self, we can also create a local strong + // variable to be captured by this block. + FLTEnsureToRunOnMainQueue(^{ [self.channel setStreamHandler:handler]; completion(); - }]; + }); } @end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.h similarity index 72% rename from packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h rename to packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.h index 70c9f868eda9..6677505671a3 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.h @@ -4,6 +4,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + /** * A thread safe wrapper for FlutterResult that can be called from any thread, by dispatching its * underlying engine calls to the main thread. @@ -13,13 +15,13 @@ /** * Gets the original FlutterResult object wrapped by this FLTThreadSafeFlutterResult instance. */ -@property(readonly, nonatomic, nonnull) FlutterResult flutterResult; +@property(readonly, nonatomic) FlutterResult flutterResult; /** * Initializes with a FlutterResult object. * @param result The FlutterResult object that the result will be given to. */ -- (nonnull instancetype)initWithResult:(nonnull FlutterResult)result; +- (instancetype)initWithResult:(FlutterResult)result; /** * Sends a successful result on the main thread without any data. @@ -30,18 +32,24 @@ * Sends a successful result on the main thread with data. * @param data Result data that is send to the Flutter Dart side. */ -- (void)sendSuccessWithData:(nonnull id)data; +- (void)sendSuccessWithData:(id)data; /** * Sends an NSError as result on the main thread. * @param error Error that will be send as FlutterError. */ -- (void)sendError:(nonnull NSError *)error; +- (void)sendError:(NSError *)error; + +/** + * Sends a FlutterError as result on the main thread. + * @param flutterError FlutterError that will be sent to the Flutter Dart side. + */ +- (void)sendFlutterError:(FlutterError *)flutterError; /** * Sends a FlutterError as result on the main thread. */ -- (void)sendErrorWithCode:(nonnull NSString *)code +- (void)sendErrorWithCode:(NSString *)code message:(nullable NSString *)message details:(nullable id)details; @@ -50,3 +58,5 @@ */ - (void)sendNotImplemented; @end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.m similarity index 68% rename from packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m rename to packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.m index 821d7561f782..283a0d6bc164 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.m @@ -4,7 +4,7 @@ #import "FLTThreadSafeFlutterResult.h" #import -#import "QueueHelper.h" +#import "QueueUtils.h" @implementation FLTThreadSafeFlutterResult { } @@ -39,6 +39,10 @@ - (void)sendErrorWithCode:(NSString *)code [self send:flutterError]; } +- (void)sendFlutterError:(FlutterError *)flutterError { + [self send:flutterError]; +} + - (void)sendNotImplemented { [self send:FlutterMethodNotImplemented]; } @@ -47,9 +51,14 @@ - (void)sendNotImplemented { * Sends result to flutterResult on the main thread. */ - (void)send:(id _Nullable)result { - [QueueHelper ensureToRunOnMainQueue:^{ + FLTEnsureToRunOnMainQueue(^{ + // WARNING: Should not use weak self, because `FlutterResult`s are passed as arguments + // (retained within call stack, but not in the heap). FLTEnsureToRunOnMainQueue may trigger a + // context switch (when calling from background thread), in which case using weak self will + // always result in a nil self. Alternative to using strong self, we can also create a local + // strong variable to be captured by this block. self.flutterResult(result); - }]; + }); } @end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.h similarity index 100% rename from packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.h rename to packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.h diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.m similarity index 78% rename from packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m rename to packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.m index ad4da87c8e91..df7c169bd43f 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeMethodChannel.m +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.m @@ -3,7 +3,7 @@ // found in the LICENSE file. #import "FLTThreadSafeMethodChannel.h" -#import "QueueHelper.h" +#import "QueueUtils.h" @interface FLTThreadSafeMethodChannel () @property(nonatomic, strong) FlutterMethodChannel *channel; @@ -20,9 +20,10 @@ - (instancetype)initWithMethodChannel:(FlutterMethodChannel *)channel { } - (void)invokeMethod:(NSString *)method arguments:(id)arguments { - [QueueHelper ensureToRunOnMainQueue:^{ - [self.channel invokeMethod:method arguments:arguments]; - }]; + __weak typeof(self) weakSelf = self; + FLTEnsureToRunOnMainQueue(^{ + [weakSelf.channel invokeMethod:method arguments:arguments]; + }); } @end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.h similarity index 100% rename from packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.h rename to packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.h diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.m similarity index 61% rename from packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m rename to packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.m index 5eb2443e4ee2..b82d566d740b 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeTextureRegistry.m +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.m @@ -3,7 +3,7 @@ // found in the LICENSE file. #import "FLTThreadSafeTextureRegistry.h" -#import "QueueHelper.h" +#import "QueueUtils.h" @interface FLTThreadSafeTextureRegistry () @property(nonatomic, strong) NSObject *registry; @@ -21,21 +21,26 @@ - (instancetype)initWithTextureRegistry:(NSObject *)regi - (void)registerTexture:(NSObject *)texture completion:(void (^)(int64_t))completion { - [QueueHelper ensureToRunOnMainQueue:^{ - completion([self.registry registerTexture:texture]); - }]; + __weak typeof(self) weakSelf = self; + FLTEnsureToRunOnMainQueue(^{ + typeof(self) strongSelf = weakSelf; + if (!strongSelf) return; + completion([strongSelf.registry registerTexture:texture]); + }); } - (void)textureFrameAvailable:(int64_t)textureId { - [QueueHelper ensureToRunOnMainQueue:^{ - [self.registry textureFrameAvailable:textureId]; - }]; + __weak typeof(self) weakSelf = self; + FLTEnsureToRunOnMainQueue(^{ + [weakSelf.registry textureFrameAvailable:textureId]; + }); } - (void)unregisterTexture:(int64_t)textureId { - [QueueHelper ensureToRunOnMainQueue:^{ - [self.registry unregisterTexture:textureId]; - }]; + __weak typeof(self) weakSelf = self; + FLTEnsureToRunOnMainQueue(^{ + [weakSelf.registry unregisterTexture:textureId]; + }); } @end diff --git a/packages/camera/camera/ios/Classes/QueueHelper.h b/packages/camera/camera_avfoundation/ios/Classes/QueueUtils.h similarity index 61% rename from packages/camera/camera/ios/Classes/QueueHelper.h rename to packages/camera/camera_avfoundation/ios/Classes/QueueUtils.h index dc7373220521..a7e22da716d0 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.h +++ b/packages/camera/camera_avfoundation/ios/Classes/QueueUtils.h @@ -7,17 +7,13 @@ NS_ASSUME_NONNULL_BEGIN /// Queue-specific context data to be associated with the capture session queue. -extern const char *FLTCaptureSessionQueueSpecific; - -/// A class that contains dispatch queue related helper functions. -@interface QueueHelper : NSObject +extern const char* FLTCaptureSessionQueueSpecific; /// Ensures the given block to be run on the main queue. -/// If caller site is already on the main queue, the block will be run synchronously. Otherwise, the -/// block will be dispatched asynchronously to the main queue. +/// If caller site is already on the main queue, the block will be run +/// synchronously. Otherwise, the block will be dispatched asynchronously to the +/// main queue. /// @param block the block to be run on the main queue. -+ (void)ensureToRunOnMainQueue:(void (^)(void))block; - -@end +extern void FLTEnsureToRunOnMainQueue(dispatch_block_t block); NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/QueueHelper.m b/packages/camera/camera_avfoundation/ios/Classes/QueueUtils.m similarity index 75% rename from packages/camera/camera/ios/Classes/QueueHelper.m rename to packages/camera/camera_avfoundation/ios/Classes/QueueUtils.m index 2cef7b677bfa..1fd54cd52cb3 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.m +++ b/packages/camera/camera_avfoundation/ios/Classes/QueueUtils.m @@ -2,18 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import "QueueHelper.h" +#import "QueueUtils.h" const char *FLTCaptureSessionQueueSpecific = "capture_session_queue"; -@implementation QueueHelper - -+ (void)ensureToRunOnMainQueue:(void (^)(void))block { +void FLTEnsureToRunOnMainQueue(dispatch_block_t block) { if (!NSThread.isMainThread) { dispatch_async(dispatch_get_main_queue(), block); } else { block(); } } - -@end diff --git a/packages/camera/camera/ios/Classes/camera-umbrella.h b/packages/camera/camera_avfoundation/ios/Classes/camera_avfoundation-umbrella.h similarity index 87% rename from packages/camera/camera/ios/Classes/camera-umbrella.h rename to packages/camera/camera_avfoundation/ios/Classes/camera_avfoundation-umbrella.h index 5c39401e6261..f8464aaae3dc 100644 --- a/packages/camera/camera/ios/Classes/camera-umbrella.h +++ b/packages/camera/camera_avfoundation/ios/Classes/camera_avfoundation-umbrella.h @@ -3,7 +3,7 @@ // found in the LICENSE file. #import -#import +#import FOUNDATION_EXPORT double cameraVersionNumber; FOUNDATION_EXPORT const unsigned char cameraVersionString[]; diff --git a/packages/camera/camera/ios/camera.podspec b/packages/camera/camera_avfoundation/ios/camera_avfoundation.podspec similarity index 76% rename from packages/camera/camera/ios/camera.podspec rename to packages/camera/camera_avfoundation/ios/camera_avfoundation.podspec index 098b011a47a9..27f569c8b9be 100644 --- a/packages/camera/camera/ios/camera.podspec +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation.podspec @@ -2,7 +2,7 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| - s.name = 'camera' + s.name = 'camera_avfoundation' s.version = '0.0.1' s.summary = 'Flutter Camera' s.description = <<-DESC @@ -11,13 +11,13 @@ A Flutter plugin to use the camera from your Flutter app. s.homepage = 'https://github.com/flutter/plugins' s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/camera' } - s.documentation_url = 'https://pub.dev/packages/camera' + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/camera_avfoundation' } + s.documentation_url = 'https://pub.dev/packages/camera_avfoundation' s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' s.module_map = 'Classes/CameraPlugin.modulemap' s.dependency 'Flutter' s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/camera/camera_avfoundation/lib/camera_avfoundation.dart b/packages/camera/camera_avfoundation/lib/camera_avfoundation.dart new file mode 100644 index 000000000000..e07a440e84f1 --- /dev/null +++ b/packages/camera/camera_avfoundation/lib/camera_avfoundation.dart @@ -0,0 +1,5 @@ +// 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. + +export 'src/avfoundation_camera.dart'; diff --git a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart new file mode 100644 index 000000000000..9bdadfb4536f --- /dev/null +++ b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart @@ -0,0 +1,594 @@ +// 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:math'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_transform/stream_transform.dart'; + +import 'type_conversion.dart'; +import 'utils.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/camera_avfoundation'); + +/// An iOS implementation of [CameraPlatform] based on AVFoundation. +class AVFoundationCamera extends CameraPlatform { + /// Registers this class as the default instance of [CameraPlatform]. + static void registerWith() { + CameraPlatform.instance = AVFoundationCamera(); + } + + final Map _channels = {}; + + /// The name of the channel that device events from the platform side are + /// sent on. + @visibleForTesting + static const String deviceEventChannelName = + 'plugins.flutter.io/camera_avfoundation/fromPlatform'; + + /// The controller we need to broadcast the different events coming + /// from handleMethodCall, specific to camera events. + /// + /// It is a `broadcast` because multiple controllers will connect to + /// different stream views of this Controller. + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + final StreamController cameraEventStreamController = + StreamController.broadcast(); + + /// The controller we need to broadcast the different events coming + /// from handleMethodCall, specific to general device events. + /// + /// It is a `broadcast` because multiple controllers will connect to + /// different stream views of this Controller. + late final StreamController _deviceEventStreamController = + _createDeviceEventStreamController(); + + StreamController _createDeviceEventStreamController() { + // Set up the method handler lazily. + const MethodChannel channel = MethodChannel(deviceEventChannelName); + channel.setMethodCallHandler(_handleDeviceMethodCall); + return StreamController.broadcast(); + } + + // The stream to receive frames from the native code. + StreamSubscription? _platformImageStreamSubscription; + + // The stream for vending frames to platform interface clients. + StreamController? _frameStreamController; + + Stream _cameraEvents(int cameraId) => + cameraEventStreamController.stream + .where((CameraEvent event) => event.cameraId == cameraId); + + @override + Future> availableCameras() async { + try { + final List>? cameras = await _channel + .invokeListMethod>('availableCameras'); + + if (cameras == null) { + return []; + } + + return cameras.map((Map camera) { + return CameraDescription( + name: camera['name']! as String, + lensDirection: + parseCameraLensDirection(camera['lensFacing']! as String), + sensorOrientation: camera['sensorOrientation']! as int, + ); + }).toList(); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset? resolutionPreset, { + bool enableAudio = false, + }) async { + try { + final Map? reply = await _channel + .invokeMapMethod('create', { + 'cameraName': cameraDescription.name, + 'resolutionPreset': resolutionPreset != null + ? _serializeResolutionPreset(resolutionPreset) + : null, + 'enableAudio': enableAudio, + }); + + return reply!['cameraId']! as int; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future initializeCamera( + int cameraId, { + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) { + _channels.putIfAbsent(cameraId, () { + final MethodChannel channel = MethodChannel( + 'plugins.flutter.io/camera_avfoundation/camera$cameraId'); + channel.setMethodCallHandler( + (MethodCall call) => handleCameraMethodCall(call, cameraId)); + return channel; + }); + + final Completer completer = Completer(); + + onCameraInitialized(cameraId).first.then((CameraInitializedEvent value) { + completer.complete(); + }); + + _channel.invokeMapMethod( + 'initialize', + { + 'cameraId': cameraId, + 'imageFormatGroup': imageFormatGroup.name(), + }, + ).catchError( + // TODO(srawlins): This should return a value of the future's type. This + // will fail upcoming analysis checks with + // https://github.com/flutter/flutter/issues/105750. + // ignore: body_might_complete_normally_catch_error + (Object error, StackTrace stackTrace) { + if (error is! PlatformException) { + throw error; + } + completer.completeError( + CameraException(error.code, error.message), + stackTrace, + ); + }, + ); + + return completer.future; + } + + @override + Future dispose(int cameraId) async { + if (_channels.containsKey(cameraId)) { + final MethodChannel? cameraChannel = _channels[cameraId]; + cameraChannel?.setMethodCallHandler(null); + _channels.remove(cameraId); + } + + await _channel.invokeMethod( + 'dispose', + {'cameraId': cameraId}, + ); + } + + @override + Stream onCameraInitialized(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraResolutionChanged(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraClosing(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraError(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onVideoRecordedEvent(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onDeviceOrientationChanged() { + return _deviceEventStreamController.stream + .whereType(); + } + + @override + Future lockCaptureOrientation( + int cameraId, + DeviceOrientation orientation, + ) async { + await _channel.invokeMethod( + 'lockCaptureOrientation', + { + 'cameraId': cameraId, + 'orientation': serializeDeviceOrientation(orientation) + }, + ); + } + + @override + Future unlockCaptureOrientation(int cameraId) async { + await _channel.invokeMethod( + 'unlockCaptureOrientation', + {'cameraId': cameraId}, + ); + } + + @override + Future takePicture(int cameraId) async { + final String? path = await _channel.invokeMethod( + 'takePicture', + {'cameraId': cameraId}, + ); + + if (path == null) { + throw CameraException( + 'INVALID_PATH', + 'The platform "$defaultTargetPlatform" did not return a path while reporting success. The platform should always return a valid path or report an error.', + ); + } + + return XFile(path); + } + + @override + Future prepareForVideoRecording() => + _channel.invokeMethod('prepareForVideoRecording'); + + @override + Future startVideoRecording(int cameraId, + {Duration? maxVideoDuration}) async { + await _channel.invokeMethod( + 'startVideoRecording', + { + 'cameraId': cameraId, + 'maxVideoDuration': maxVideoDuration?.inMilliseconds, + }, + ); + } + + @override + Future stopVideoRecording(int cameraId) async { + final String? path = await _channel.invokeMethod( + 'stopVideoRecording', + {'cameraId': cameraId}, + ); + + if (path == null) { + throw CameraException( + 'INVALID_PATH', + 'The platform "$defaultTargetPlatform" did not return a path while reporting success. The platform should always return a valid path or report an error.', + ); + } + + return XFile(path); + } + + @override + Future pauseVideoRecording(int cameraId) => _channel.invokeMethod( + 'pauseVideoRecording', + {'cameraId': cameraId}, + ); + + @override + Future resumeVideoRecording(int cameraId) => + _channel.invokeMethod( + 'resumeVideoRecording', + {'cameraId': cameraId}, + ); + + @override + Stream onStreamedFrameAvailable(int cameraId, + {CameraImageStreamOptions? options}) { + _frameStreamController = StreamController( + onListen: _onFrameStreamListen, + onPause: _onFrameStreamPauseResume, + onResume: _onFrameStreamPauseResume, + onCancel: _onFrameStreamCancel, + ); + return _frameStreamController!.stream; + } + + void _onFrameStreamListen() { + _startPlatformStream(); + } + + Future _startPlatformStream() async { + await _channel.invokeMethod('startImageStream'); + const EventChannel cameraEventChannel = + EventChannel('plugins.flutter.io/camera_avfoundation/imageStream'); + _platformImageStreamSubscription = + cameraEventChannel.receiveBroadcastStream().listen((dynamic imageData) { + try { + _channel.invokeMethod('receivedImageStreamData'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + _frameStreamController! + .add(cameraImageFromPlatformData(imageData as Map)); + }); + } + + FutureOr _onFrameStreamCancel() async { + await _channel.invokeMethod('stopImageStream'); + await _platformImageStreamSubscription?.cancel(); + _platformImageStreamSubscription = null; + _frameStreamController = null; + } + + void _onFrameStreamPauseResume() { + throw CameraException('InvalidCall', + 'Pause and resume are not supported for onStreamedFrameAvailable'); + } + + @override + Future setFlashMode(int cameraId, FlashMode mode) => + _channel.invokeMethod( + 'setFlashMode', + { + 'cameraId': cameraId, + 'mode': _serializeFlashMode(mode), + }, + ); + + @override + Future setExposureMode(int cameraId, ExposureMode mode) => + _channel.invokeMethod( + 'setExposureMode', + { + 'cameraId': cameraId, + 'mode': serializeExposureMode(mode), + }, + ); + + @override + Future setExposurePoint(int cameraId, Point? point) { + assert(point == null || point.x >= 0 && point.x <= 1); + assert(point == null || point.y >= 0 && point.y <= 1); + + return _channel.invokeMethod( + 'setExposurePoint', + { + 'cameraId': cameraId, + 'reset': point == null, + 'x': point?.x, + 'y': point?.y, + }, + ); + } + + @override + Future getMinExposureOffset(int cameraId) async { + final double? minExposureOffset = await _channel.invokeMethod( + 'getMinExposureOffset', + {'cameraId': cameraId}, + ); + + return minExposureOffset!; + } + + @override + Future getMaxExposureOffset(int cameraId) async { + final double? maxExposureOffset = await _channel.invokeMethod( + 'getMaxExposureOffset', + {'cameraId': cameraId}, + ); + + return maxExposureOffset!; + } + + @override + Future getExposureOffsetStepSize(int cameraId) async { + final double? stepSize = await _channel.invokeMethod( + 'getExposureOffsetStepSize', + {'cameraId': cameraId}, + ); + + return stepSize!; + } + + @override + Future setExposureOffset(int cameraId, double offset) async { + final double? appliedOffset = await _channel.invokeMethod( + 'setExposureOffset', + { + 'cameraId': cameraId, + 'offset': offset, + }, + ); + + return appliedOffset!; + } + + @override + Future setFocusMode(int cameraId, FocusMode mode) => + _channel.invokeMethod( + 'setFocusMode', + { + 'cameraId': cameraId, + 'mode': serializeFocusMode(mode), + }, + ); + + @override + Future setFocusPoint(int cameraId, Point? point) { + assert(point == null || point.x >= 0 && point.x <= 1); + assert(point == null || point.y >= 0 && point.y <= 1); + + return _channel.invokeMethod( + 'setFocusPoint', + { + 'cameraId': cameraId, + 'reset': point == null, + 'x': point?.x, + 'y': point?.y, + }, + ); + } + + @override + Future getMaxZoomLevel(int cameraId) async { + final double? maxZoomLevel = await _channel.invokeMethod( + 'getMaxZoomLevel', + {'cameraId': cameraId}, + ); + + return maxZoomLevel!; + } + + @override + Future getMinZoomLevel(int cameraId) async { + final double? minZoomLevel = await _channel.invokeMethod( + 'getMinZoomLevel', + {'cameraId': cameraId}, + ); + + return minZoomLevel!; + } + + @override + Future setZoomLevel(int cameraId, double zoom) async { + try { + await _channel.invokeMethod( + 'setZoomLevel', + { + 'cameraId': cameraId, + 'zoom': zoom, + }, + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future pausePreview(int cameraId) async { + await _channel.invokeMethod( + 'pausePreview', + {'cameraId': cameraId}, + ); + } + + @override + Future resumePreview(int cameraId) async { + await _channel.invokeMethod( + 'resumePreview', + {'cameraId': cameraId}, + ); + } + + @override + Widget buildPreview(int cameraId) { + return Texture(textureId: cameraId); + } + + /// Returns the flash mode as a String. + String _serializeFlashMode(FlashMode flashMode) { + switch (flashMode) { + case FlashMode.off: + return 'off'; + case FlashMode.auto: + return 'auto'; + case FlashMode.always: + return 'always'; + case FlashMode.torch: + return 'torch'; + default: + throw ArgumentError('Unknown FlashMode value'); + } + } + + /// Returns the resolution preset as a String. + String _serializeResolutionPreset(ResolutionPreset resolutionPreset) { + switch (resolutionPreset) { + case ResolutionPreset.max: + return 'max'; + case ResolutionPreset.ultraHigh: + return 'ultraHigh'; + case ResolutionPreset.veryHigh: + return 'veryHigh'; + case ResolutionPreset.high: + return 'high'; + case ResolutionPreset.medium: + return 'medium'; + case ResolutionPreset.low: + return 'low'; + default: + throw ArgumentError('Unknown ResolutionPreset value'); + } + } + + /// Converts messages received from the native platform into device events. + Future _handleDeviceMethodCall(MethodCall call) async { + switch (call.method) { + case 'orientation_changed': + _deviceEventStreamController.add(DeviceOrientationChangedEvent( + deserializeDeviceOrientation( + call.arguments['orientation']! as String))); + break; + default: + throw MissingPluginException(); + } + } + + /// Converts messages received from the native platform into camera events. + /// + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + Future handleCameraMethodCall(MethodCall call, int cameraId) async { + switch (call.method) { + case 'initialized': + cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + call.arguments['previewWidth']! as double, + call.arguments['previewHeight']! as double, + deserializeExposureMode(call.arguments['exposureMode']! as String), + call.arguments['exposurePointSupported']! as bool, + deserializeFocusMode(call.arguments['focusMode']! as String), + call.arguments['focusPointSupported']! as bool, + )); + break; + case 'resolution_changed': + cameraEventStreamController.add(CameraResolutionChangedEvent( + cameraId, + call.arguments['captureWidth']! as double, + call.arguments['captureHeight']! as double, + )); + break; + case 'camera_closing': + cameraEventStreamController.add(CameraClosingEvent( + cameraId, + )); + break; + case 'video_recorded': + cameraEventStreamController.add(VideoRecordedEvent( + cameraId, + XFile(call.arguments['path']! as String), + call.arguments['maxVideoDuration'] != null + ? Duration( + milliseconds: call.arguments['maxVideoDuration']! as int) + : null, + )); + break; + case 'error': + cameraEventStreamController.add(CameraErrorEvent( + cameraId, + call.arguments['description']! as String, + )); + break; + default: + throw MissingPluginException(); + } + } +} diff --git a/packages/camera/camera_avfoundation/lib/src/type_conversion.dart b/packages/camera/camera_avfoundation/lib/src/type_conversion.dart new file mode 100644 index 000000000000..c2a539a63dab --- /dev/null +++ b/packages/camera/camera_avfoundation/lib/src/type_conversion.dart @@ -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. + +// 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'; + +/// Converts method channel call [data] for `receivedImageStreamData` to a +/// [CameraImageData]. +CameraImageData cameraImageFromPlatformData(Map data) { + return CameraImageData( + format: _cameraImageFormatFromPlatformData(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) => _cameraImagePlaneFromPlatformData( + planeData as Map)))); +} + +CameraImageFormat _cameraImageFormatFromPlatformData(dynamic data) { + return CameraImageFormat(_imageFormatGroupFromPlatformData(data), raw: data); +} + +ImageFormatGroup _imageFormatGroupFromPlatformData(dynamic data) { + switch (data) { + case 875704438: // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange + return ImageFormatGroup.yuv420; + + case 1111970369: // kCVPixelFormatType_32BGRA + return ImageFormatGroup.bgra8888; + } + + return ImageFormatGroup.unknown; +} + +CameraImagePlane _cameraImagePlaneFromPlatformData(Map data) { + return CameraImagePlane( + 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?); +} diff --git a/packages/camera/camera_avfoundation/lib/src/utils.dart b/packages/camera/camera_avfoundation/lib/src/utils.dart new file mode 100644 index 000000000000..663ec6da7a97 --- /dev/null +++ b/packages/camera/camera_avfoundation/lib/src/utils.dart @@ -0,0 +1,51 @@ +// 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_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; + +/// Parses a string into a corresponding CameraLensDirection. +CameraLensDirection parseCameraLensDirection(String string) { + switch (string) { + case 'front': + return CameraLensDirection.front; + case 'back': + return CameraLensDirection.back; + case 'external': + return CameraLensDirection.external; + } + throw ArgumentError('Unknown CameraLensDirection value'); +} + +/// Returns the device orientation as a String. +String serializeDeviceOrientation(DeviceOrientation orientation) { + switch (orientation) { + case DeviceOrientation.portraitUp: + return 'portraitUp'; + case DeviceOrientation.portraitDown: + return 'portraitDown'; + case DeviceOrientation.landscapeRight: + return 'landscapeRight'; + case DeviceOrientation.landscapeLeft: + return 'landscapeLeft'; + default: + throw ArgumentError('Unknown DeviceOrientation value'); + } +} + +/// Returns the device orientation for a given String. +DeviceOrientation deserializeDeviceOrientation(String str) { + switch (str) { + case 'portraitUp': + return DeviceOrientation.portraitUp; + case 'portraitDown': + return DeviceOrientation.portraitDown; + case 'landscapeRight': + return DeviceOrientation.landscapeRight; + case 'landscapeLeft': + return DeviceOrientation.landscapeLeft; + default: + throw ArgumentError('"$str" is not a valid DeviceOrientation value'); + } +} diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml new file mode 100644 index 000000000000..f394d59e81d5 --- /dev/null +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -0,0 +1,30 @@ +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.8+6 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: camera + platforms: + ios: + pluginClass: CameraPlugin + dartPluginClass: AVFoundationCamera + +dependencies: + camera_platform_interface: ^2.2.0 + flutter: + sdk: flutter + stream_transform: ^2.0.0 + +dev_dependencies: + async: ^2.5.0 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter diff --git a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart new file mode 100644 index 000000000000..60109a4172b7 --- /dev/null +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart @@ -0,0 +1,1094 @@ +// 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:math'; + +import 'package:async/async.dart'; +import 'package:camera_avfoundation/src/avfoundation_camera.dart'; +import 'package:camera_avfoundation/src/utils.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'method_channel_mock.dart'; + +const String _channelName = 'plugins.flutter.io/camera_avfoundation'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('registers instance', () async { + AVFoundationCamera.registerWith(); + expect(CameraPlatform.instance, isA()); + }); + + test('registration does not set message handlers', () async { + AVFoundationCamera.registerWith(); + + // Setting up a handler requires bindings to be initialized, and since + // 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) {}); + expect(response, null); + }); + + group('Creation, Initialization & Disposal Tests', () { + test('Should send creation data and receive back a camera id', () async { + // Arrange + final MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: _channelName, + methods: { + 'create': { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + } + }); + final AVFoundationCamera camera = AVFoundationCamera(); + + // Act + final int cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0), + ResolutionPreset.high, + ); + + // Assert + expect(cameraMockChannel.log, [ + isMethodCall( + 'create', + arguments: { + 'cameraName': 'Test', + 'resolutionPreset': 'high', + 'enableAudio': false + }, + ), + ]); + expect(cameraId, 1); + }); + + test('Should throw CameraException when create throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: _channelName, methods: { + 'create': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + final AVFoundationCamera camera = AVFoundationCamera(); + + // Act + expect( + () => camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ), + throwsA( + isA() + .having( + (CameraException e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((CameraException e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test('Should throw CameraException when create throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: _channelName, methods: { + 'create': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + final AVFoundationCamera camera = AVFoundationCamera(); + + // Act + expect( + () => camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ), + throwsA( + isA() + .having( + (CameraException e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((CameraException e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test( + 'Should throw CameraException when initialize throws a PlatformException', + () { + // Arrange + MethodChannelMock( + channelName: _channelName, + methods: { + 'initialize': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }, + ); + final AVFoundationCamera camera = AVFoundationCamera(); + + // Act + expect( + () => camera.initializeCamera(0), + throwsA( + isA() + .having( + (CameraException e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having( + (CameraException e) => e.description, + 'description', + 'Mock error message used during testing.', + ), + ), + ); + }, + ); + + test('Should send initialization data', () async { + // Arrange + final MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: _channelName, + methods: { + 'create': { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + }, + 'initialize': null + }); + final AVFoundationCamera camera = AVFoundationCamera(); + final int cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + + // Act + final Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + )); + await initializeFuture; + + // Assert + expect(cameraId, 1); + expect(cameraMockChannel.log, [ + anything, + isMethodCall( + 'initialize', + arguments: { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + }, + ), + ]); + }); + + test('Should send a disposal call on dispose', () async { + // Arrange + final MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: _channelName, + methods: { + 'create': {'cameraId': 1}, + 'initialize': null, + 'dispose': {'cameraId': 1} + }); + + final AVFoundationCamera camera = AVFoundationCamera(); + final int cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + final Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + )); + await initializeFuture; + + // Act + await camera.dispose(cameraId); + + // Assert + expect(cameraId, 1); + expect(cameraMockChannel.log, [ + anything, + anything, + isMethodCall( + 'dispose', + arguments: {'cameraId': 1}, + ), + ]); + }); + }); + + group('Event Tests', () { + late AVFoundationCamera camera; + late int cameraId; + setUp(() async { + MethodChannelMock( + channelName: _channelName, + methods: { + 'create': {'cameraId': 1}, + 'initialize': null + }, + ); + camera = AVFoundationCamera(); + cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + final Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + )); + await initializeFuture; + }); + + test('Should receive initialized event', () async { + // Act + final Stream eventStream = + camera.onCameraInitialized(cameraId); + final StreamQueue streamQueue = + StreamQueue(eventStream); + + // Emit test events + final CameraInitializedEvent event = CameraInitializedEvent( + cameraId, + 3840, + 2160, + ExposureMode.auto, + true, + FocusMode.auto, + true, + ); + await camera.handleCameraMethodCall( + MethodCall('initialized', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive resolution changes', () async { + // Act + final Stream resolutionStream = + camera.onCameraResolutionChanged(cameraId); + final StreamQueue streamQueue = + StreamQueue(resolutionStream); + + // Emit test events + final CameraResolutionChangedEvent fhdEvent = + CameraResolutionChangedEvent(cameraId, 1920, 1080); + final CameraResolutionChangedEvent uhdEvent = + CameraResolutionChangedEvent(cameraId, 3840, 2160); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, fhdEvent); + expect(await streamQueue.next, uhdEvent); + expect(await streamQueue.next, fhdEvent); + expect(await streamQueue.next, uhdEvent); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive camera closing events', () async { + // Act + final Stream eventStream = + camera.onCameraClosing(cameraId); + final StreamQueue streamQueue = + StreamQueue(eventStream); + + // Emit test events + final CameraClosingEvent event = CameraClosingEvent(cameraId); + await camera.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive camera error events', () async { + // Act + final Stream errorStream = + camera.onCameraError(cameraId); + final StreamQueue streamQueue = + StreamQueue(errorStream); + + // Emit test events + final CameraErrorEvent event = + CameraErrorEvent(cameraId, 'Error Description'); + await camera.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive device orientation change events', () async { + // Act + final Stream eventStream = + camera.onDeviceOrientationChanged(); + final StreamQueue streamQueue = + StreamQueue(eventStream); + + // Emit test events + const DeviceOrientationChangedEvent event = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + for (int i = 0; i < 3; i++) { + await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + .handlePlatformMessage( + AVFoundationCamera.deviceEventChannelName, + const StandardMethodCodec().encodeMethodCall( + MethodCall('orientation_changed', event.toJson())), + null); + } + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + }); + + group('Function Tests', () { + late AVFoundationCamera camera; + late int cameraId; + + setUp(() async { + MethodChannelMock( + channelName: _channelName, + methods: { + 'create': {'cameraId': 1}, + 'initialize': null + }, + ); + camera = AVFoundationCamera(); + cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + final Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add( + CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + ), + ); + await initializeFuture; + }); + + test('Should fetch CameraDescription instances for available cameras', + () async { + // Arrange + final List returnData = [ + { + 'name': 'Test 1', + 'lensFacing': 'front', + 'sensorOrientation': 1 + }, + { + 'name': 'Test 2', + 'lensFacing': 'back', + 'sensorOrientation': 2 + } + ]; + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'availableCameras': returnData}, + ); + + // Act + final List cameras = await camera.availableCameras(); + + // Assert + expect(channel.log, [ + isMethodCall('availableCameras', arguments: null), + ]); + expect(cameras.length, returnData.length); + for (int i = 0; i < returnData.length; i++) { + final CameraDescription cameraDescription = CameraDescription( + name: returnData[i]['name']! as String, + lensDirection: + parseCameraLensDirection(returnData[i]['lensFacing']! as String), + sensorOrientation: returnData[i]['sensorOrientation']! as int, + ); + expect(cameras[i], cameraDescription); + } + }); + + test( + 'Should throw CameraException when availableCameras throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: _channelName, methods: { + 'availableCameras': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + + // Act + expect( + camera.availableCameras, + throwsA( + isA() + .having( + (CameraException e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((CameraException e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test('Should take a picture and return an XFile instance', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'takePicture': '/test/path.jpg'}); + + // Act + final XFile file = await camera.takePicture(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('takePicture', arguments: { + 'cameraId': cameraId, + }), + ]); + expect(file.path, '/test/path.jpg'); + }); + + test('Should prepare for video recording', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'prepareForVideoRecording': null}, + ); + + // Act + await camera.prepareForVideoRecording(); + + // Assert + expect(channel.log, [ + isMethodCall('prepareForVideoRecording', arguments: null), + ]); + }); + + test('Should start recording a video', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'startVideoRecording': null}, + ); + + // Act + await camera.startVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('startVideoRecording', arguments: { + 'cameraId': cameraId, + 'maxVideoDuration': null, + }), + ]); + }); + + test('Should pass maxVideoDuration when starting recording a video', + () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'startVideoRecording': null}, + ); + + // Act + await camera.startVideoRecording( + cameraId, + maxVideoDuration: const Duration(seconds: 10), + ); + + // Assert + expect(channel.log, [ + isMethodCall('startVideoRecording', arguments: { + 'cameraId': cameraId, + 'maxVideoDuration': 10000 + }), + ]); + }); + + test('Should stop a video recording and return the file', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'stopVideoRecording': '/test/path.mp4'}, + ); + + // Act + final XFile file = await camera.stopVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('stopVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + expect(file.path, '/test/path.mp4'); + }); + + test('Should pause a video recording', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'pauseVideoRecording': null}, + ); + + // Act + await camera.pauseVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('pauseVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should resume a video recording', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'resumeVideoRecording': null}, + ); + + // Act + await camera.resumeVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('resumeVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the flash mode', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setFlashMode': null}, + ); + + // Act + await camera.setFlashMode(cameraId, FlashMode.torch); + await camera.setFlashMode(cameraId, FlashMode.always); + await camera.setFlashMode(cameraId, FlashMode.auto); + await camera.setFlashMode(cameraId, FlashMode.off); + + // Assert + expect(channel.log, [ + isMethodCall('setFlashMode', arguments: { + 'cameraId': cameraId, + 'mode': 'torch' + }), + isMethodCall('setFlashMode', arguments: { + 'cameraId': cameraId, + 'mode': 'always' + }), + isMethodCall('setFlashMode', + arguments: {'cameraId': cameraId, 'mode': 'auto'}), + isMethodCall('setFlashMode', + arguments: {'cameraId': cameraId, 'mode': 'off'}), + ]); + }); + + test('Should set the exposure mode', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setExposureMode': null}, + ); + + // Act + await camera.setExposureMode(cameraId, ExposureMode.auto); + await camera.setExposureMode(cameraId, ExposureMode.locked); + + // Assert + expect(channel.log, [ + isMethodCall('setExposureMode', + arguments: {'cameraId': cameraId, 'mode': 'auto'}), + isMethodCall('setExposureMode', arguments: { + 'cameraId': cameraId, + 'mode': 'locked' + }), + ]); + }); + + test('Should set the exposure point', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setExposurePoint': null}, + ); + + // Act + await camera.setExposurePoint(cameraId, const Point(0.5, 0.5)); + await camera.setExposurePoint(cameraId, null); + + // Assert + expect(channel.log, [ + isMethodCall('setExposurePoint', arguments: { + 'cameraId': cameraId, + 'x': 0.5, + 'y': 0.5, + 'reset': false + }), + isMethodCall('setExposurePoint', arguments: { + 'cameraId': cameraId, + 'x': null, + 'y': null, + 'reset': true + }), + ]); + }); + + test('Should get the min exposure offset', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'getMinExposureOffset': 2.0}, + ); + + // Act + final double minExposureOffset = + await camera.getMinExposureOffset(cameraId); + + // Assert + expect(minExposureOffset, 2.0); + expect(channel.log, [ + isMethodCall('getMinExposureOffset', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the max exposure offset', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'getMaxExposureOffset': 2.0}, + ); + + // Act + final double maxExposureOffset = + await camera.getMaxExposureOffset(cameraId); + + // Assert + expect(maxExposureOffset, 2.0); + expect(channel.log, [ + isMethodCall('getMaxExposureOffset', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the exposure offset step size', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'getExposureOffsetStepSize': 0.25}, + ); + + // Act + final double stepSize = await camera.getExposureOffsetStepSize(cameraId); + + // Assert + expect(stepSize, 0.25); + expect(channel.log, [ + isMethodCall('getExposureOffsetStepSize', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the exposure offset', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setExposureOffset': 0.6}, + ); + + // Act + final double actualOffset = await camera.setExposureOffset(cameraId, 0.5); + + // Assert + expect(actualOffset, 0.6); + expect(channel.log, [ + isMethodCall('setExposureOffset', arguments: { + 'cameraId': cameraId, + 'offset': 0.5, + }), + ]); + }); + + test('Should set the focus mode', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setFocusMode': null}, + ); + + // Act + await camera.setFocusMode(cameraId, FocusMode.auto); + await camera.setFocusMode(cameraId, FocusMode.locked); + + // Assert + expect(channel.log, [ + isMethodCall('setFocusMode', + arguments: {'cameraId': cameraId, 'mode': 'auto'}), + isMethodCall('setFocusMode', arguments: { + 'cameraId': cameraId, + 'mode': 'locked' + }), + ]); + }); + + test('Should set the exposure point', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setFocusPoint': null}, + ); + + // Act + await camera.setFocusPoint(cameraId, const Point(0.5, 0.5)); + await camera.setFocusPoint(cameraId, null); + + // Assert + expect(channel.log, [ + isMethodCall('setFocusPoint', arguments: { + 'cameraId': cameraId, + 'x': 0.5, + 'y': 0.5, + 'reset': false + }), + isMethodCall('setFocusPoint', arguments: { + 'cameraId': cameraId, + 'x': null, + 'y': null, + 'reset': true + }), + ]); + }); + + test('Should build a texture widget as preview widget', () async { + // Act + final Widget widget = camera.buildPreview(cameraId); + + // Act + expect(widget is Texture, isTrue); + expect((widget as Texture).textureId, cameraId); + }); + + test('Should throw MissingPluginException when handling unknown method', + () { + final AVFoundationCamera camera = AVFoundationCamera(); + + expect( + () => camera.handleCameraMethodCall( + const MethodCall('unknown_method'), 1), + throwsA(isA())); + }); + + test('Should get the max zoom level', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'getMaxZoomLevel': 10.0}, + ); + + // Act + final double maxZoomLevel = await camera.getMaxZoomLevel(cameraId); + + // Assert + expect(maxZoomLevel, 10.0); + expect(channel.log, [ + isMethodCall('getMaxZoomLevel', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the min zoom level', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'getMinZoomLevel': 1.0}, + ); + + // Act + final double maxZoomLevel = await camera.getMinZoomLevel(cameraId); + + // Assert + expect(maxZoomLevel, 1.0); + expect(channel.log, [ + isMethodCall('getMinZoomLevel', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the zoom level', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setZoomLevel': null}, + ); + + // Act + await camera.setZoomLevel(cameraId, 2.0); + + // Assert + expect(channel.log, [ + isMethodCall('setZoomLevel', + arguments: {'cameraId': cameraId, 'zoom': 2.0}), + ]); + }); + + test('Should throw CameraException when illegal zoom level is supplied', + () async { + // Arrange + MethodChannelMock( + channelName: _channelName, + methods: { + 'setZoomLevel': PlatformException( + code: 'ZOOM_ERROR', + message: 'Illegal zoom error', + ) + }, + ); + + // Act & assert + expect( + () => camera.setZoomLevel(cameraId, -1.0), + throwsA(isA() + .having((CameraException e) => e.code, 'code', 'ZOOM_ERROR') + .having((CameraException e) => e.description, 'description', + 'Illegal zoom error'))); + }); + + test('Should lock the capture orientation', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'lockCaptureOrientation': null}, + ); + + // Act + await camera.lockCaptureOrientation( + cameraId, DeviceOrientation.portraitUp); + + // Assert + expect(channel.log, [ + isMethodCall('lockCaptureOrientation', arguments: { + 'cameraId': cameraId, + 'orientation': 'portraitUp' + }), + ]); + }); + + test('Should unlock the capture orientation', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'unlockCaptureOrientation': null}, + ); + + // Act + await camera.unlockCaptureOrientation(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('unlockCaptureOrientation', + arguments: {'cameraId': cameraId}), + ]); + }); + + test('Should pause the camera preview', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'pausePreview': null}, + ); + + // Act + await camera.pausePreview(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('pausePreview', + arguments: {'cameraId': cameraId}), + ]); + }); + + test('Should resume the camera preview', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'resumePreview': null}, + ); + + // Act + await camera.resumePreview(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('resumePreview', + arguments: {'cameraId': cameraId}), + ]); + }); + + test('Should start streaming', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: { + 'startImageStream': null, + 'stopImageStream': null, + }, + ); + + // Act + final StreamSubscription subscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData imageData) {}); + + // Assert + expect(channel.log, [ + isMethodCall('startImageStream', arguments: null), + ]); + + subscription.cancel(); + }); + + test('Should stop streaming', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: { + 'startImageStream': null, + 'stopImageStream': null, + }, + ); + + // Act + final StreamSubscription subscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData imageData) {}); + subscription.cancel(); + + // Assert + expect(channel.log, [ + isMethodCall('startImageStream', arguments: null), + isMethodCall('stopImageStream', arguments: null), + ]); + }); + }); +} diff --git a/packages/camera/camera_avfoundation/test/method_channel_mock.dart b/packages/camera/camera_avfoundation/test/method_channel_mock.dart new file mode 100644 index 000000000000..413c10633cc1 --- /dev/null +++ b/packages/camera/camera_avfoundation/test/method_channel_mock.dart @@ -0,0 +1,39 @@ +// 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'; +import 'package:flutter_test/flutter_test.dart'; + +class MethodChannelMock { + MethodChannelMock({ + required String channelName, + this.delay, + required this.methods, + }) : methodChannel = MethodChannel(channelName) { + methodChannel.setMockMethodCallHandler(_handler); + } + + final Duration? delay; + final MethodChannel methodChannel; + final Map methods; + final List log = []; + + Future _handler(MethodCall methodCall) async { + log.add(methodCall); + + if (!methods.containsKey(methodCall.method)) { + throw MissingPluginException('No implementation found for method ' + '${methodCall.method} on channel ${methodChannel.name}'); + } + + return Future.delayed(delay ?? Duration.zero, () { + final dynamic result = methods[methodCall.method]; + if (result is Exception) { + throw result; + } + + return Future.value(result); + }); + } +} diff --git a/packages/camera/camera_avfoundation/test/type_conversion_test.dart b/packages/camera/camera_avfoundation/test/type_conversion_test.dart new file mode 100644 index 000000000000..282f4aedb21d --- /dev/null +++ b/packages/camera/camera_avfoundation/test/type_conversion_test.dart @@ -0,0 +1,60 @@ +// 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_avfoundation/src/type_conversion.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('CameraImageData can be created', () { + final CameraImageData cameraImage = + cameraImageFromPlatformData({ + 'format': 1, + 'height': 1, + 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.height, 1); + expect(cameraImage.width, 4); + expect(cameraImage.format.group, ImageFormatGroup.unknown); + expect(cameraImage.planes.length, 1); + }); + + test('CameraImageData has ImageFormatGroup.yuv420', () { + final CameraImageData cameraImage = + cameraImageFromPlatformData({ + 'format': 875704438, + 'height': 1, + 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + }); +} diff --git a/packages/camera/camera_avfoundation/test/utils_test.dart b/packages/camera/camera_avfoundation/test/utils_test.dart new file mode 100644 index 000000000000..bd28abb0dc63 --- /dev/null +++ b/packages/camera/camera_avfoundation/test/utils_test.dart @@ -0,0 +1,60 @@ +// 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_avfoundation/src/utils.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Utility methods', () { + test( + 'Should return CameraLensDirection when valid value is supplied when parsing camera lens direction', + () { + expect( + parseCameraLensDirection('back'), + CameraLensDirection.back, + ); + expect( + parseCameraLensDirection('front'), + CameraLensDirection.front, + ); + expect( + parseCameraLensDirection('external'), + CameraLensDirection.external, + ); + }); + + test( + 'Should throw ArgumentException when invalid value is supplied when parsing camera lens direction', + () { + expect( + () => parseCameraLensDirection('test'), + throwsA(isArgumentError), + ); + }); + + test('serializeDeviceOrientation() should serialize correctly', () { + expect(serializeDeviceOrientation(DeviceOrientation.portraitUp), + 'portraitUp'); + expect(serializeDeviceOrientation(DeviceOrientation.portraitDown), + 'portraitDown'); + expect(serializeDeviceOrientation(DeviceOrientation.landscapeRight), + 'landscapeRight'); + expect(serializeDeviceOrientation(DeviceOrientation.landscapeLeft), + 'landscapeLeft'); + }); + + test('deserializeDeviceOrientation() should deserialize correctly', () { + expect(deserializeDeviceOrientation('portraitUp'), + DeviceOrientation.portraitUp); + expect(deserializeDeviceOrientation('portraitDown'), + DeviceOrientation.portraitDown); + expect(deserializeDeviceOrientation('landscapeRight'), + DeviceOrientation.landscapeRight); + expect(deserializeDeviceOrientation('landscapeLeft'), + DeviceOrientation.landscapeLeft); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 6ed662b2afa0..d410304970cf 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,25 @@ +## 2.2.2 + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. + +## 2.2.1 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. +* Fixes avoid_redundant_argument_values lint warnings and minor typos. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/104231). +* Ignores missing return warnings in preparation for [upcoming analysis changes](https://github.com/flutter/flutter/issues/105750). + +## 2.2.0 + +* Adds image streaming to the platform interface. +* Removes unnecessary imports. + +## 2.1.6 + +* Adopts `Object.hash`. +* Removes obsolete dependency on `pedantic`. + ## 2.1.5 * Fixes asynchronous exceptions handling of the `initializeCamera` method. diff --git a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart index a91e538e4be5..a6ace8f9ae74 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:camera_platform_interface/src/types/focus_mode.dart'; import 'package:flutter/foundation.dart' show immutable; import '../../camera_platform_interface.dart'; @@ -117,14 +116,15 @@ class CameraInitializedEvent extends CameraEvent { focusPointSupported == other.focusPointSupported; @override - int get hashCode => - super.hashCode ^ - previewWidth.hashCode ^ - previewHeight.hashCode ^ - exposureMode.hashCode ^ - exposurePointSupported.hashCode ^ - focusMode.hashCode ^ - focusPointSupported.hashCode; + int get hashCode => Object.hash( + super.hashCode, + previewWidth, + previewHeight, + exposureMode, + exposurePointSupported, + focusMode, + focusPointSupported, + ); } /// An event fired when the resolution preset of the camera has changed. @@ -171,8 +171,7 @@ class CameraResolutionChangedEvent extends CameraEvent { captureHeight == other.captureHeight; @override - int get hashCode => - super.hashCode ^ captureWidth.hashCode ^ captureHeight.hashCode; + int get hashCode => Object.hash(super.hashCode, captureWidth, captureHeight); } /// An event fired when the camera is going to close. @@ -239,7 +238,7 @@ class CameraErrorEvent extends CameraEvent { description == other.description; @override - int get hashCode => super.hashCode ^ description.hashCode; + int get hashCode => Object.hash(super.hashCode, description); } /// An event fired when a video has finished recording. @@ -284,6 +283,5 @@ class VideoRecordedEvent extends CameraEvent { maxVideoDuration == other.maxVideoDuration; @override - int get hashCode => - super.hashCode ^ file.hashCode ^ maxVideoDuration.hashCode; + int get hashCode => Object.hash(super.hashCode, file, maxVideoDuration); } diff --git a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart index d6bb5df05980..65a378f16f12 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart @@ -2,10 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:camera_platform_interface/src/utils/utils.dart'; import 'package:flutter/foundation.dart' show immutable; import 'package:flutter/services.dart'; +import '../utils/utils.dart'; + /// Generic Event coming from the native side of Camera, /// not related to a specific camera module. /// 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 ec84c204b2c6..37c00d64ede2 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 @@ -5,17 +5,15 @@ import 'dart:async'; import 'dart:math'; -import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_platform_interface/src/events/device_event.dart'; -import 'package:camera_platform_interface/src/types/focus_mode.dart'; -import 'package:camera_platform_interface/src/types/image_format_group.dart'; -import 'package:camera_platform_interface/src/utils/utils.dart'; -import 'package:cross_file/cross_file.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_transform/stream_transform.dart'; +import '../../camera_platform_interface.dart'; +import '../utils/utils.dart'; +import 'type_conversion.dart'; + const MethodChannel _channel = MethodChannel('plugins.flutter.io/camera'); /// An implementation of [CameraPlatform] that uses method channels. @@ -52,6 +50,12 @@ class MethodChannelCamera extends CameraPlatform { final StreamController deviceEventStreamController = StreamController.broadcast(); + // The stream to receive frames from the native code. + StreamSubscription? _platformImageStreamSubscription; + + // The stream for vending frames to platform interface clients. + StreamController? _frameStreamController; + Stream _cameraEvents(int cameraId) => cameraEventStreamController.stream .where((CameraEvent event) => event.cameraId == cameraId); @@ -114,10 +118,10 @@ class MethodChannelCamera extends CameraPlatform { return channel; }); - final Completer _completer = Completer(); + final Completer completer = Completer(); onCameraInitialized(cameraId).first.then((CameraInitializedEvent value) { - _completer.complete(); + completer.complete(); }); _channel.invokeMapMethod( @@ -127,18 +131,22 @@ class MethodChannelCamera extends CameraPlatform { 'imageFormatGroup': imageFormatGroup.name(), }, ).catchError( + // TODO(srawlins): This should return a value of the future's type. This + // will fail upcoming analysis checks with + // https://github.com/flutter/flutter/issues/105750. + // ignore: body_might_complete_normally_catch_error (Object error, StackTrace stackTrace) { if (error is! PlatformException) { throw error; } - _completer.completeError( + completer.completeError( CameraException(error.code, error.message), stackTrace, ); }, ); - return _completer.future; + return completer.future; } @override @@ -271,6 +279,52 @@ class MethodChannelCamera extends CameraPlatform { {'cameraId': cameraId}, ); + @override + Stream onStreamedFrameAvailable(int cameraId, + {CameraImageStreamOptions? options}) { + _frameStreamController = StreamController( + onListen: _onFrameStreamListen, + onPause: _onFrameStreamPauseResume, + onResume: _onFrameStreamPauseResume, + onCancel: _onFrameStreamCancel, + ); + return _frameStreamController!.stream; + } + + void _onFrameStreamListen() { + _startPlatformStream(); + } + + Future _startPlatformStream() async { + await _channel.invokeMethod('startImageStream'); + const EventChannel cameraEventChannel = + EventChannel('plugins.flutter.io/camera/imageStream'); + _platformImageStreamSubscription = + cameraEventChannel.receiveBroadcastStream().listen((dynamic imageData) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + try { + _channel.invokeMethod('receivedImageStreamData'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + _frameStreamController! + .add(cameraImageFromPlatformData(imageData as Map)); + }); + } + + FutureOr _onFrameStreamCancel() async { + await _channel.invokeMethod('stopImageStream'); + await _platformImageStreamSubscription?.cancel(); + _platformImageStreamSubscription = null; + _frameStreamController = null; + } + + void _onFrameStreamPauseResume() { + throw CameraException('InvalidCall', + 'Pause and resume are not supported for onStreamedFrameAvailable'); + } + @override Future setFlashMode(int cameraId, FlashMode mode) => _channel.invokeMethod( diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/type_conversion.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/type_conversion.dart new file mode 100644 index 000000000000..8b360077305c --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/type_conversion.dart @@ -0,0 +1,63 @@ +// 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:flutter/foundation.dart'; + +import '../types/types.dart'; + +/// Converts method channel call [data] for `receivedImageStreamData` to a +/// [CameraImageData]. +CameraImageData cameraImageFromPlatformData(Map data) { + return CameraImageData( + format: _cameraImageFormatFromPlatformData(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) => _cameraImagePlaneFromPlatformData( + planeData as Map)))); +} + +CameraImageFormat _cameraImageFormatFromPlatformData(dynamic data) { + return CameraImageFormat(_imageFormatGroupFromPlatformData(data), raw: data); +} + +ImageFormatGroup _imageFormatGroupFromPlatformData(dynamic data) { + if (defaultTargetPlatform == TargetPlatform.android) { + switch (data) { + case 35: // android.graphics.ImageFormat.YUV_420_888 + return ImageFormatGroup.yuv420; + case 256: // android.graphics.ImageFormat.JPEG + return ImageFormatGroup.jpeg; + } + } + + if (defaultTargetPlatform == TargetPlatform.iOS) { + switch (data) { + case 875704438: // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange + return ImageFormatGroup.yuv420; + + case 1111970369: // kCVPixelFormatType_32BGRA + return ImageFormatGroup.bgra8888; + } + } + + return ImageFormatGroup.unknown; +} + +CameraImagePlane _cameraImagePlaneFromPlatformData(Map data) { + return CameraImagePlane( + 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?); +} 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 0d240496086d..b086dc87851f 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 @@ -5,17 +5,13 @@ import 'dart:async'; import 'dart:math'; -import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_platform_interface/src/events/device_event.dart'; -import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; -import 'package:camera_platform_interface/src/types/exposure_mode.dart'; -import 'package:camera_platform_interface/src/types/focus_mode.dart'; -import 'package:camera_platform_interface/src/types/image_format_group.dart'; -import 'package:cross_file/cross_file.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import '../../camera_platform_interface.dart'; +import '../method_channel/method_channel_camera.dart'; + /// The interface that implementations of camera must implement. /// /// Platform implementations should extend this class rather than implement it as `camera` @@ -154,6 +150,21 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('resumeVideoRecording() is not implemented.'); } + /// A new streamed frame is available. + /// + /// Listening to this stream will start streaming, and canceling will stop. + /// Pausing will throw a [CameraException], as pausing the stream would cause + /// very high memory usage; to temporarily stop receiving frames, cancel, then + /// listen again later. + /// + /// + // TODO(bmparr): Add options to control streaming settings (e.g., + // resolution and FPS). + Stream onStreamedFrameAvailable(int cameraId, + {CameraImageStreamOptions? options}) { + throw UnimplementedError('onStreamedFrameAvailable() is not implemented.'); + } + /// Sets the flash mode for the selected camera. /// On Web [FlashMode.auto] corresponds to [FlashMode.always]. Future setFlashMode(int cameraId, FlashMode mode) { diff --git a/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart b/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart index df7263631252..0167cf9e17a1 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart @@ -50,7 +50,7 @@ class CameraDescription { lensDirection == other.lensDirection; @override - int get hashCode => name.hashCode ^ lensDirection.hashCode; + int get hashCode => Object.hash(name, lensDirection); @override String toString() { diff --git a/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart b/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart new file mode 100644 index 000000000000..4bafe270fa49 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart @@ -0,0 +1,128 @@ +// 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:flutter/foundation.dart'; + +import '../../camera_platform_interface.dart'; + +/// Options for configuring camera streaming. +/// +/// Currently unused; this exists for future-proofing of the platform interface +/// API. +@immutable +class CameraImageStreamOptions {} + +/// A single color plane of image data. +/// +/// The number and meaning of the planes in an image are determined by its +/// format. +@immutable +class CameraImagePlane { + /// Creates a new instance with the given bytes and optional metadata. + const CameraImagePlane({ + required this.bytes, + required this.bytesPerRow, + this.bytesPerPixel, + this.height, + this.width, + }); + + /// Bytes representing this plane. + final Uint8List bytes; + + /// The row stride for this color plane, in bytes. + final int bytesPerRow; + + /// The distance between adjacent pixel samples in bytes, when available. + final int? bytesPerPixel; + + /// Height of the pixel buffer, when available. + final int? height; + + /// Width of the pixel buffer, when available. + final int? width; +} + +/// Describes how pixels are represented in an image. +@immutable +class CameraImageFormat { + /// Create a new format with the given cross-platform group and raw underyling + /// platform identifier. + const CameraImageFormat(this.group, {required this.raw}); + + /// Describes the format group the raw image format falls into. + final ImageFormatGroup group; + + /// Raw version of the format from the underlying platform. + /// + /// On Android, this should be an `int` from class + /// `android.graphics.ImageFormat`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat + /// + /// On iOS, this should be a `FourCharCode` constant from Pixel Format + /// Identifiers. See + /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers + final dynamic raw; +} + +/// 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 +/// [CameraImagePlane] that describes the layout of the pixel data in that +/// plane. [CameraImageData] is not directly usable as a UI resource. +/// +/// Although not all image formats are planar on all platforms, this class +/// treats 1-dimensional images as single planar images. +@immutable +class CameraImageData { + /// Creates a new instance with the given format, planes, and metadata. + const CameraImageData({ + required this.format, + required this.planes, + required this.height, + required this.width, + this.lensAperture, + this.sensorExposureTime, + this.sensorSensitivity, + }); + + /// 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 CameraImageFormat 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_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart index 0c24839d6445..3eb09fcb833c 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/types.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/types.dart @@ -4,6 +4,7 @@ export 'camera_description.dart'; export 'camera_exception.dart'; +export 'camera_image_data.dart'; export 'exposure_mode.dart'; export 'flash_mode.dart'; export 'focus_mode.dart'; diff --git a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart index 663ec6da7a97..d86880afd216 100644 --- a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart +++ b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart @@ -2,9 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/services.dart'; +import '../../camera_platform_interface.dart'; + /// Parses a string into a corresponding CameraLensDirection. CameraLensDirection parseCameraLensDirection(String string) { switch (string) { diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index b28008d8d58e..e87679261c5a 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.1.5 +version: 2.2.2 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.0.0" + flutter: ">=2.10.0" dependencies: cross_file: ^0.3.1 @@ -21,4 +21,3 @@ dev_dependencies: async: ^2.5.0 flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart index 3060089bef40..eab518ce3b23 100644 --- a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -18,7 +18,14 @@ void main() { test('Cannot be implemented with `implements`', () { expect(() { CameraPlatform.instance = ImplementsCameraPlatform(); - }, throwsNoSuchMethodError); + // In versions of `package:plugin_platform_interface` prior to fixing + // https://github.com/flutter/flutter/issues/109339, an attempt to + // implement a platform interface using `implements` would sometimes + // throw a `NoSuchMethodError` and other times throw an + // `AssertionError`. After the issue is fixed, an `AssertionError` will + // always be thrown. For the purpose of this test, we don't really care + // what exception is thrown, so just allow any exception. + }, throwsA(anything)); }); test('Can be extended', () { diff --git a/packages/camera/camera_platform_interface/test/events/camera_event_test.dart b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart index a46486ed252c..074f203bea21 100644 --- a/packages/camera/camera_platform_interface/test/events/camera_event_test.dart +++ b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart @@ -3,8 +3,6 @@ // found in the LICENSE file. import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_platform_interface/src/types/exposure_mode.dart'; -import 'package:camera_platform_interface/src/types/focus_mode.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -137,13 +135,14 @@ void main() { test('hashCode should match hashCode of all properties', () { const CameraInitializedEvent event = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); - final int expectedHashCode = event.cameraId.hashCode ^ - event.previewWidth.hashCode ^ - event.previewHeight.hashCode ^ - event.exposureMode.hashCode ^ - event.exposurePointSupported.hashCode ^ - event.focusMode.hashCode ^ - event.focusPointSupported.hashCode; + final int expectedHashCode = Object.hash( + event.cameraId.hashCode, + event.previewWidth, + event.previewHeight, + event.exposureMode, + event.exposurePointSupported, + event.focusMode, + event.focusPointSupported); expect(event.hashCode, expectedHashCode); }); @@ -223,9 +222,11 @@ void main() { test('hashCode should match hashCode of all properties', () { const CameraResolutionChangedEvent event = CameraResolutionChangedEvent(1, 1024, 640); - final int expectedHashCode = event.cameraId.hashCode ^ - event.captureWidth.hashCode ^ - event.captureHeight.hashCode; + final int expectedHashCode = Object.hash( + event.cameraId.hashCode, + event.captureWidth, + event.captureHeight, + ); expect(event.hashCode, expectedHashCode); }); @@ -328,7 +329,7 @@ void main() { test('hashCode should match hashCode of all properties', () { const CameraErrorEvent event = CameraErrorEvent(1, 'Error'); final int expectedHashCode = - event.cameraId.hashCode ^ event.description.hashCode; + Object.hash(event.cameraId.hashCode, event.description); expect(event.hashCode, expectedHashCode); }); diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index 27fe7c6b7166..60f42fd4af4a 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 @@ -7,11 +7,8 @@ import 'dart:math'; import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_platform_interface/src/events/device_event.dart'; import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; -import 'package:camera_platform_interface/src/types/focus_mode.dart'; import 'package:camera_platform_interface/src/utils/utils.dart'; -import 'package:flutter/services.dart' hide DeviceOrientation; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -957,7 +954,6 @@ void main() { 'setZoomLevel': PlatformException( code: 'ZOOM_ERROR', message: 'Illegal zoom error', - details: null, ) }, ); @@ -1041,6 +1037,52 @@ void main() { arguments: {'cameraId': cameraId}), ]); }); + + test('Should start streaming', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'startImageStream': null, + 'stopImageStream': null, + }, + ); + + // Act + final StreamSubscription subscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData imageData) {}); + + // Assert + expect(channel.log, [ + isMethodCall('startImageStream', arguments: null), + ]); + + subscription.cancel(); + }); + + test('Should stop streaming', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'startImageStream': null, + 'stopImageStream': null, + }, + ); + + // Act + final StreamSubscription subscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData imageData) {}); + subscription.cancel(); + + // Assert + expect(channel.log, [ + isMethodCall('startImageStream', arguments: null), + isMethodCall('stopImageStream', arguments: null), + ]); + }); }); }); } diff --git a/packages/camera/camera_platform_interface/test/method_channel/type_conversion_test.dart b/packages/camera/camera_platform_interface/test/method_channel/type_conversion_test.dart new file mode 100644 index 000000000000..4818074ec767 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/method_channel/type_conversion_test.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. + +// 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:camera_platform_interface/src/method_channel/type_conversion.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('CameraImageData can be created', () { + final CameraImageData cameraImage = + cameraImageFromPlatformData({ + 'format': 35, + 'height': 1, + 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.height, 1); + expect(cameraImage.width, 4); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + expect(cameraImage.planes.length, 1); + }); + + test('CameraImageData has ImageFormatGroup.yuv420 for iOS', () { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + + final CameraImageData cameraImage = + cameraImageFromPlatformData({ + 'format': 875704438, + 'height': 1, + 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + }); + + test('CameraImageData has ImageFormatGroup.yuv420 for Android', () { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final CameraImageData cameraImage = + cameraImageFromPlatformData({ + 'format': 35, + 'height': 1, + 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/camera_description_test.dart b/packages/camera/camera_platform_interface/test/types/camera_description_test.dart index 3d3aaaeb4086..a86df031ac3a 100644 --- a/packages/camera/camera_platform_interface/test/types/camera_description_test.dart +++ b/packages/camera/camera_platform_interface/test/types/camera_description_test.dart @@ -97,15 +97,15 @@ void main() { expect(firstDescription == secondDescription, true); }); - test('hashCode should match hashCode of all properties', () { + test('hashCode should match hashCode of all equality-tested properties', + () { const CameraDescription description = CameraDescription( name: 'Test', lensDirection: CameraLensDirection.front, sensorOrientation: 0, ); - final int expectedHashCode = description.name.hashCode ^ - description.lensDirection.hashCode ^ - description.sensorOrientation.hashCode; + final int expectedHashCode = + Object.hash(description.name, description.lensDirection); expect(description.hashCode, expectedHashCode); }); diff --git a/packages/camera/camera_platform_interface/test/types/camera_image_data_test.dart b/packages/camera/camera_platform_interface/test/types/camera_image_data_test.dart new file mode 100644 index 000000000000..d8c582d74844 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/camera_image_data_test.dart @@ -0,0 +1,40 @@ +// 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'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('CameraImageData can be created', () { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + final CameraImageData cameraImage = CameraImageData( + format: const CameraImageFormat(ImageFormatGroup.jpeg, raw: 42), + height: 100, + width: 200, + lensAperture: 1.8, + sensorExposureTime: 11, + sensorSensitivity: 92.0, + planes: [ + CameraImagePlane( + bytes: Uint8List.fromList([1, 2, 3, 4]), + bytesPerRow: 4, + bytesPerPixel: 2, + height: 100, + width: 200) + ], + ); + expect(cameraImage.format.group, ImageFormatGroup.jpeg); + expect(cameraImage.lensAperture, 1.8); + expect(cameraImage.sensorExposureTime, 11); + expect(cameraImage.sensorSensitivity, 92.0); + expect(cameraImage.height, 100); + expect(cameraImage.width, 200); + expect(cameraImage.planes.length, 1); + }); +} diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index c72e31fc0f18..f4989cfd5bff 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,3 +1,28 @@ +## 0.3.0+1 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. +* Fixes avoid_redundant_argument_values lint warnings and minor typos. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). + +## 0.3.0 + +* **BREAKING CHANGE**: Renames error code `cameraPermission` to `CameraAccessDenied` to be consistent with other platforms. + +## 0.2.1+6 + +* Minor fixes for new analysis options. + +## 0.2.1+5 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.2.1+4 + +* Migrates from `ui.hash*` to `Object.hash*`. +* Updates minimum Flutter version for changes in 0.2.1+3. + ## 0.2.1+3 * Internal code cleanup for stricter analysis options. diff --git a/packages/camera/camera_web/example/README.md b/packages/camera/camera_web/example/README.md index 8a6e74b107ea..0e51ae5ecbd2 100644 --- a/packages/camera/camera_web/example/README.md +++ b/packages/camera/camera_web/example/README.md @@ -1,4 +1,14 @@ -# Testing +# 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. + +## Testing This package uses `package:integration_test` to run its tests in a web browser. @@ -6,4 +16,4 @@ See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Te in the Flutter wiki for instructions to setup and run the tests in this package. Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) -for more info. \ No newline at end of file +for more info. diff --git a/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart b/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart index 112683bdad98..e89018f7c512 100644 --- a/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart @@ -46,7 +46,7 @@ void main() { testWidgets('permissionDenied', (WidgetTester tester) async { expect( CameraErrorCode.permissionDenied.toString(), - equals('cameraPermission'), + equals('CameraAccessDenied'), ); }); diff --git a/packages/camera/camera_web/example/integration_test/camera_options_test.dart b/packages/camera/camera_web/example/integration_test/camera_options_test.dart index ee63d87e9f07..6619ff41e03c 100644 --- a/packages/camera/camera_web/example/integration_test/camera_options_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_options_test.dart @@ -30,7 +30,7 @@ void main() { testWidgets('supports value equality', (WidgetTester tester) async { expect( CameraOptions( - audio: const AudioConstraints(enabled: false), + audio: const AudioConstraints(), video: VideoConstraints( facingMode: FacingModeConstraint(CameraType.environment), width: @@ -42,7 +42,7 @@ void main() { ), equals( CameraOptions( - audio: const AudioConstraints(enabled: false), + audio: const AudioConstraints(), video: VideoConstraints( facingMode: FacingModeConstraint(CameraType.environment), width: const VideoSizeConstraint( diff --git a/packages/camera/camera_web/example/integration_test/camera_service_test.dart b/packages/camera/camera_web/example/integration_test/camera_service_test.dart index 6bc03429940c..27199320fc56 100644 --- a/packages/camera/camera_web/example/integration_test/camera_service_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_service_test.dart @@ -4,6 +4,8 @@ import 'dart:html'; import 'dart:js_util' as js_util; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; @@ -22,7 +24,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('CameraService', () { - const int cameraId = 0; + const int cameraId = 1; late Window window; late Navigator navigator; diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 4e8050eef6a8..50451b9778af 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -53,7 +53,7 @@ void main() { }); setUpAll(() { - registerFallbackValue(MockCameraOptions()); + registerFallbackValue(MockCameraOptions()); }); group('initialize', () { @@ -1477,7 +1477,7 @@ void main() { }); group('dispose', () { - testWidgets('resets the video element\'s source', + testWidgets("resets the video element's source", (WidgetTester tester) async { final Camera camera = Camera( textureId: textureId, diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 9f21eb43fb34..e3f11383469c 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -4,6 +4,8 @@ import 'dart:async'; import 'dart:html'; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import import 'dart:ui'; import 'package:async/async.dart'; @@ -24,7 +26,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('CameraPlugin', () { - const int cameraId = 0; + const int cameraId = 1; late Window window; late Navigator navigator; @@ -76,9 +78,9 @@ void main() { }); setUpAll(() { - registerFallbackValue(MockMediaStreamTrack()); - registerFallbackValue(MockCameraOptions()); - registerFallbackValue(FlashMode.off); + registerFallbackValue(MockMediaStreamTrack()); + registerFallbackValue(MockCameraOptions()); + registerFallbackValue(FlashMode.off); }); testWidgets('CameraPlugin is the live instance', @@ -498,7 +500,7 @@ void main() { isA().having( (CameraException e) => e.code, 'code', - exception.code.toString(), + exception.code, ), ), ); @@ -591,7 +593,7 @@ void main() { (Camera camera) => camera.options, 'options', CameraOptions( - audio: const AudioConstraints(enabled: false), + audio: const AudioConstraints(), video: VideoConstraints( facingMode: FacingModeConstraint(CameraType.user), width: VideoSizeConstraint( @@ -759,7 +761,7 @@ void main() { isA().having( (PlatformException e) => e.code, 'code', - exception.name.toString(), + exception.name, ), ), ); @@ -2495,7 +2497,7 @@ void main() { equals( CameraErrorEvent( cameraId, - 'Error code: ${CameraErrorCode.abort}, error message: The video element\'s source has not fully loaded.', + "Error code: ${CameraErrorCode.abort}, error message: The video element's source has not fully loaded.", ), ), ); diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index 3d9550fb7ab8..521c4bf5a18d 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -113,8 +113,8 @@ class FakeElementStream extends Fake final Stream _stream; @override - StreamSubscription listen(void onData(T event)?, - {Function? onError, void onDone()?, bool? cancelOnError}) { + StreamSubscription listen(void Function(T event)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) { return _stream.listen( onData, onError: onError, diff --git a/packages/camera/camera_web/example/lib/main.dart b/packages/camera/camera_web/example/lib/main.dart index ab04ce2ca2c7..670891fa5009 100644 --- a/packages/camera/camera_web/example/lib/main.dart +++ b/packages/camera/camera_web/example/lib/main.dart @@ -4,10 +4,13 @@ import 'package:flutter/material.dart'; -void main() => runApp(MyApp()); +void main() => runApp(const MyApp()); /// App for testing class MyApp extends StatelessWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return const Directionality( diff --git a/packages/camera/camera_web/example/pubspec.yaml b/packages/camera/camera_web/example/pubspec.yaml index 0457e8fdfcf2..e82bbe392ceb 100644 --- a/packages/camera/camera_web/example/pubspec.yaml +++ b/packages/camera/camera_web/example/pubspec.yaml @@ -3,19 +3,22 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" dependencies: flutter: sdk: flutter dev_dependencies: + async: ^2.5.0 + camera_platform_interface: ^2.1.0 camera_web: path: ../ + cross_file: ^0.3.1 flutter_driver: sdk: flutter flutter_test: sdk: flutter integration_test: sdk: flutter - mocktail: ^0.1.4 + mocktail: ^0.3.0 diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 71368c65e99d..13ef21b1ea46 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -7,11 +7,11 @@ import 'dart:html' as html; import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_web/src/camera_service.dart'; -import 'package:camera_web/src/types/types.dart'; import 'package:flutter/foundation.dart'; +import 'camera_service.dart'; import 'shims/dart_ui.dart' as ui; +import 'types/types.dart'; String _getViewType(int cameraId) => 'plugins.flutter.io/camera_$cameraId'; @@ -138,6 +138,9 @@ class Camera { /// A builder to merge a list of blobs into a single blob. @visibleForTesting + // TODO(stuartmorgan): Remove this 'ignore' once we don't analyze using 2.10 + // any more. It's a false positive that is fixed in later versions. + // ignore: prefer_function_declarations_over_variables html.Blob Function(List blobs, String type) blobBuilder = (List blobs, String type) => html.Blob(blobs, type); diff --git a/packages/camera/camera_web/lib/src/camera_service.dart b/packages/camera/camera_web/lib/src/camera_service.dart index f15845cf823b..6e20c7d74f78 100644 --- a/packages/camera/camera_web/lib/src/camera_service.dart +++ b/packages/camera/camera_web/lib/src/camera_service.dart @@ -3,15 +3,18 @@ // found in the LICENSE file. import 'dart:html' as html; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/shims/dart_js_util.dart'; -import 'package:camera_web/src/types/types.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'camera.dart'; +import 'shims/dart_js_util.dart'; +import 'types/types.dart'; + /// A service to fetch, map camera settings and /// obtain the camera stream. class CameraService { @@ -82,7 +85,7 @@ class CameraService { throw CameraWebException( cameraId, CameraErrorCode.type, - 'The camera options are incorrect or attempted' + 'The camera options are incorrect or attempted ' 'to access the media input from an insecure context.', ); case 'AbortError': diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 6f9f10d68f84..d440653cd424 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -7,14 +7,15 @@ import 'dart:html' as html; import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/camera_service.dart'; -import 'package:camera_web/src/types/types.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:stream_transform/stream_transform.dart'; +import 'camera.dart'; +import 'camera_service.dart'; +import 'types/types.dart'; + // The default error message, when the error is an empty string. // See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/message const String _kDefaultErrorMessage = @@ -290,7 +291,7 @@ class CameraPlugin extends CameraPlatform { cameraEventStreamController.add( CameraErrorEvent( cameraId, - 'Error code: ${CameraErrorCode.abort}, error message: The video element\'s source has not fully loaded.', + "Error code: ${CameraErrorCode.abort}, error message: The video element's source has not fully loaded.", ), ); }); @@ -400,7 +401,7 @@ class CameraPlugin extends CameraPlatform { // This wrapper allows use of both the old and new APIs. dynamic fullScreen() => documentElement.requestFullscreen(); await fullScreen(); - await screenOrientation.lock(orientationType.toString()); + await screenOrientation.lock(orientationType); } else { throw PlatformException( code: CameraErrorCode.orientationNotSupported.toString(), diff --git a/packages/camera/camera_web/lib/src/shims/dart_ui_fake.dart b/packages/camera/camera_web/lib/src/shims/dart_ui_fake.dart index 8757ca22be17..40d8f1903111 100644 --- a/packages/camera/camera_web/lib/src/shims/dart_ui_fake.dart +++ b/packages/camera/camera_web/lib/src/shims/dart_ui_fake.dart @@ -11,10 +11,10 @@ import 'dart:html' as html; // ignore_for_file: camel_case_types /// Shim for web_ui engine.PlatformViewRegistry -/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62 +/// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/ui.dart#L62 class platformViewRegistry { /// Shim for registerViewFactory - /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72 + /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/ui.dart#L72 static bool registerViewFactory( String viewTypeId, html.Element Function(int viewId) viewFactory) { return false; @@ -22,10 +22,10 @@ class platformViewRegistry { } /// Shim for web_ui engine.AssetManager. -/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12 +/// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L12 class webOnlyAssetManager { /// Shim for getAssetUrl. - /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L45 + /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L45 static String getAssetUrl(String asset) => ''; } diff --git a/packages/camera/camera_web/lib/src/types/camera_error_code.dart b/packages/camera/camera_web/lib/src/types/camera_error_code.dart index 2e8a49b63873..8f1831f79cf5 100644 --- a/packages/camera/camera_web/lib/src/types/camera_error_code.dart +++ b/packages/camera/camera_web/lib/src/types/camera_error_code.dart @@ -32,7 +32,7 @@ class CameraErrorCode { /// The camera cannot be used or the permission /// to access the camera is not granted. static const CameraErrorCode permissionDenied = - CameraErrorCode._('cameraPermission'); + CameraErrorCode._('CameraAccessDenied'); /// The camera options are incorrect or attempted /// to access the media input from an insecure context. diff --git a/packages/camera/camera_web/lib/src/types/camera_metadata.dart b/packages/camera/camera_web/lib/src/types/camera_metadata.dart index c42dd3ad8b75..e5c6b3875b6a 100644 --- a/packages/camera/camera_web/lib/src/types/camera_metadata.dart +++ b/packages/camera/camera_web/lib/src/types/camera_metadata.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart'; /// Metadata used along the camera description @@ -38,5 +36,5 @@ class CameraMetadata { } @override - int get hashCode => hashValues(deviceId.hashCode, facingMode.hashCode); + int get hashCode => Object.hash(deviceId.hashCode, facingMode.hashCode); } diff --git a/packages/camera/camera_web/lib/src/types/camera_options.dart b/packages/camera/camera_web/lib/src/types/camera_options.dart index 8fa40bdc1bb8..08491b56081b 100644 --- a/packages/camera/camera_web/lib/src/types/camera_options.dart +++ b/packages/camera/camera_web/lib/src/types/camera_options.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart'; /// Options used to create a camera with the given @@ -50,7 +48,7 @@ class CameraOptions { } @override - int get hashCode => hashValues(audio, video); + int get hashCode => Object.hash(audio, video); } /// Indicates whether the audio track is requested. @@ -140,7 +138,7 @@ class VideoConstraints { } @override - int get hashCode => hashValues(facingMode, width, height, deviceId); + int get hashCode => Object.hash(facingMode, width, height, deviceId); } /// The camera type used in [FacingModeConstraint]. @@ -213,7 +211,7 @@ class FacingModeConstraint { } @override - int get hashCode => hashValues(ideal, exact); + int get hashCode => Object.hash(ideal, exact); } /// The size of the requested video track used in @@ -272,5 +270,5 @@ class VideoSizeConstraint { } @override - int get hashCode => hashValues(minimum, ideal, maximum); + int get hashCode => Object.hash(minimum, ideal, maximum); } diff --git a/packages/camera/camera_web/lib/src/types/camera_web_exception.dart b/packages/camera/camera_web/lib/src/types/camera_web_exception.dart index c21106cc462e..e6c6d7a0fed0 100644 --- a/packages/camera/camera_web/lib/src/types/camera_web_exception.dart +++ b/packages/camera/camera_web/lib/src/types/camera_web_exception.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:camera_web/src/types/types.dart'; +import 'types.dart'; /// An exception thrown when the camera with id [cameraId] reports /// an initialization, configuration or video streaming error, diff --git a/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart b/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart index 9a868d2bc0dc..d20bd25108bb 100644 --- a/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart +++ b/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:html' as html; -import 'dart:ui' show hashValues; import 'package:flutter/foundation.dart'; @@ -46,5 +45,5 @@ class ZoomLevelCapability { } @override - int get hashCode => hashValues(minimum, maximum, videoTrack); + int get hashCode => Object.hash(minimum, maximum, videoTrack); } diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index 6d6f110157bc..ef9c45c71796 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.2.1+3 +version: 0.3.0+1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/camera/camera_windows/CHANGELOG.md b/packages/camera/camera_windows/CHANGELOG.md index 1318780830f8..71c5d56524a6 100644 --- a/packages/camera/camera_windows/CHANGELOG.md +++ b/packages/camera/camera_windows/CHANGELOG.md @@ -1,3 +1,44 @@ +## 0.2.1+2 + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. +* Updates minimum Flutter version to 2.10. + +## 0.2.1+1 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 0.2.1 + +* Adds a check for string size before Win32 MultiByte <-> WideChar conversions + +## 0.2.0 + +**BREAKING CHANGES**: + * `CameraException.code` now has value `"CameraAccessDenied"` if camera access permission was denied. + * `CameraException.code` now has value `"camera_error"` if error occurs during capture. + +## 0.1.0+5 + +* Fixes bugs in in error handling. + +## 0.1.0+4 + +* Allows retrying camera initialization after error. + +## 0.1.0+3 + +* Updates the README to better explain how to use the unendorsed package. + +## 0.1.0+2 + +* Updates references to the obsolete master branch. + +## 0.1.0+1 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 0.1.0 * Initial release diff --git a/packages/camera/camera_windows/README.md b/packages/camera/camera_windows/README.md index dc27bcc85e9d..4b66ad3dfe32 100644 --- a/packages/camera/camera_windows/README.md +++ b/packages/camera/camera_windows/README.md @@ -10,8 +10,10 @@ See [missing implementations and limitations](#missing-features-on-the-windows-p ### Depend on the package This package is not an [endorsed][endorsed-federated-plugin] -implementation of the [`camera`][camera] plugin, so you'll need to -[add it explicitly][install]. +implementation of the [`camera`][camera] plugin, so in addition to depending +on [`camera`][camera] you'll need to +[add `camera_windows` to your pubspec.yaml explicitly][install]. +Once you do, you can use the [`camera`][camera] APIs as you normally would. ## Missing features on the Windows platform @@ -63,4 +65,4 @@ disposing of the camera is the only way to reset the situation. [install]: https://pub.dev/packages/camera_windows/install [camera-control-issue]: https://github.com/flutter/flutter/issues/97537 [device-orientation-issue]: https://github.com/flutter/flutter/issues/97540 -[image-streams-issue]: https://github.com/flutter/flutter/issues/97542 \ No newline at end of file +[image-streams-issue]: https://github.com/flutter/flutter/issues/97542 diff --git a/packages/camera/camera_windows/example/README.md b/packages/camera/camera_windows/example/README.md index ee7326472eaf..96b8bb17dbff 100644 --- a/packages/camera/camera_windows/example/README.md +++ b/packages/camera/camera_windows/example/README.md @@ -1,3 +1,9 @@ -# camera_windows_example +# Platform Implementation Test App -Demonstrates how to use the camera_windows plugin. +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/camera/camera_windows/example/lib/main.dart b/packages/camera/camera_windows/example/lib/main.dart index b73e00cac52b..d27edb860975 100644 --- a/packages/camera/camera_windows/example/lib/main.dart +++ b/packages/camera/camera_windows/example/lib/main.dart @@ -9,11 +9,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } /// Example app for Camera Windows plugin. class MyApp extends StatefulWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + @override State createState() => _MyAppState(); } @@ -111,7 +114,6 @@ class _MyAppState extends State { await CameraPlatform.instance.initializeCamera( cameraId, - imageFormatGroup: ImageFormatGroup.unknown, ); final CameraInitializedEvent event = await initialized; @@ -185,8 +187,8 @@ class _MyAppState extends State { } Future _takePicture() async { - final XFile _file = await CameraPlatform.instance.takePicture(_cameraId); - _showInSnackBar('Picture captured to: ${_file.path}'); + final XFile file = await CameraPlatform.instance.takePicture(_cameraId); + _showInSnackBar('Picture captured to: ${file.path}'); } Future _recordTimed(int seconds) async { @@ -226,10 +228,10 @@ class _MyAppState extends State { if (!_recording) { await CameraPlatform.instance.startVideoRecording(_cameraId); } else { - final XFile _file = + final XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); - _showInSnackBar('Video captured to: ${_file.path}'); + _showInSnackBar('Video captured to: ${file.path}'); } if (mounted) { @@ -426,7 +428,6 @@ class _MyAppState extends State { vertical: 10, ), child: Align( - alignment: Alignment.center, child: Container( constraints: const BoxConstraints( maxHeight: 500, diff --git a/packages/camera/camera_windows/example/pubspec.yaml b/packages/camera/camera_windows/example/pubspec.yaml index aa806a292333..80ce958a0e84 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.8.0" + flutter: ">=2.10.0" dependencies: camera_platform_interface: ^2.1.2 @@ -19,6 +19,7 @@ dependencies: sdk: flutter dev_dependencies: + async: ^2.5.0 flutter_test: sdk: flutter integration_test: diff --git a/packages/camera/camera_windows/lib/camera_windows.dart b/packages/camera/camera_windows/lib/camera_windows.dart index 33f8bfb68fac..14134479994b 100644 --- a/packages/camera/camera_windows/lib/camera_windows.dart +++ b/packages/camera/camera_windows/lib/camera_windows.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:cross_file/cross_file.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -23,7 +22,7 @@ class CameraWindows extends CameraPlatform { final MethodChannel pluginChannel = const MethodChannel('plugins.flutter.io/camera_windows'); - /// Camera specific method channels to allow comminicating with specific cameras. + /// Camera specific method channels to allow communicating with specific cameras. final Map _cameraChannels = {}; /// The controller that broadcasts events coming from handleCameraMethodCall diff --git a/packages/camera/camera_windows/pubspec.yaml b/packages/camera/camera_windows/pubspec.yaml index 1081c3dfc01f..1eab9fa108ef 100644 --- a/packages/camera/camera_windows/pubspec.yaml +++ b/packages/camera/camera_windows/pubspec.yaml @@ -1,12 +1,12 @@ name: camera_windows description: A Flutter plugin for getting information about and controlling the camera on Windows. -version: 0.1.0 -repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera_windows +repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 +version: 0.2.1+2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/camera/camera_windows/windows/camera.cpp b/packages/camera/camera_windows/windows/camera.cpp index c21f8ab0af78..6a0944747908 100644 --- a/packages/camera/camera_windows/windows/camera.cpp +++ b/packages/camera/camera_windows/windows/camera.cpp @@ -16,6 +16,25 @@ constexpr char kVideoRecordedEvent[] = "video_recorded"; constexpr char kCameraClosingEvent[] = "camera_closing"; constexpr char kErrorEvent[] = "error"; +// Camera error codes +constexpr char kCameraAccessDenied[] = "CameraAccessDenied"; +constexpr char kCameraError[] = "camera_error"; +constexpr char kPluginDisposed[] = "plugin_disposed"; + +std::string GetErrorCode(CameraResult result) { + assert(result != CameraResult::kSuccess); + + switch (result) { + case CameraResult::kAccessDenied: + return kCameraAccessDenied; + + case CameraResult::kSuccess: + case CameraResult::kError: + default: + return kCameraError; + } +} + CameraImpl::CameraImpl(const std::string& device_id) : device_id_(device_id), Camera(device_id) {} @@ -24,21 +43,21 @@ CameraImpl::~CameraImpl() { OnCameraClosing(); capture_controller_ = nullptr; - SendErrorForPendingResults("plugin_disposed", + SendErrorForPendingResults(kPluginDisposed, "Plugin disposed before request was handled"); } -void CameraImpl::InitCamera(flutter::TextureRegistrar* texture_registrar, +bool CameraImpl::InitCamera(flutter::TextureRegistrar* texture_registrar, flutter::BinaryMessenger* messenger, bool record_audio, ResolutionPreset resolution_preset) { auto capture_controller_factory = std::make_unique(); - InitCamera(std::move(capture_controller_factory), texture_registrar, - messenger, record_audio, resolution_preset); + return InitCamera(std::move(capture_controller_factory), texture_registrar, + messenger, record_audio, resolution_preset); } -void CameraImpl::InitCamera( +bool CameraImpl::InitCamera( std::unique_ptr capture_controller_factory, flutter::TextureRegistrar* texture_registrar, flutter::BinaryMessenger* messenger, bool record_audio, @@ -47,8 +66,8 @@ void CameraImpl::InitCamera( messenger_ = messenger; capture_controller_ = capture_controller_factory->CreateCaptureController(this); - capture_controller_->InitCaptureDevice(texture_registrar, device_id_, - record_audio, resolution_preset); + return capture_controller_->InitCaptureDevice( + texture_registrar, device_id_, record_audio, resolution_preset); } bool CameraImpl::AddPendingResult( @@ -85,9 +104,9 @@ bool CameraImpl::HasPendingResultByType(PendingResultType type) const { } void CameraImpl::SendErrorForPendingResults(const std::string& error_code, - const std::string& descripion) { + const std::string& description) { for (const auto& pending_result : pending_results_) { - pending_result.second->Error(error_code, descripion); + pending_result.second->Error(error_code, description); } pending_results_.clear(); } @@ -121,11 +140,13 @@ void CameraImpl::OnCreateCaptureEngineSucceeded(int64_t texture_id) { } } -void CameraImpl::OnCreateCaptureEngineFailed(const std::string& error) { +void CameraImpl::OnCreateCaptureEngineFailed(CameraResult result, + const std::string& error) { auto pending_result = GetPendingResultByType(PendingResultType::kCreateCamera); if (pending_result) { - pending_result->Error("camera_error", error); + std::string error_code = GetErrorCode(result); + pending_result->Error(error_code, error); } } @@ -141,10 +162,12 @@ void CameraImpl::OnStartPreviewSucceeded(int32_t width, int32_t height) { } }; -void CameraImpl::OnStartPreviewFailed(const std::string& error) { +void CameraImpl::OnStartPreviewFailed(CameraResult result, + const std::string& error) { auto pending_result = GetPendingResultByType(PendingResultType::kInitialize); if (pending_result) { - pending_result->Error("camera_error", error); + std::string error_code = GetErrorCode(result); + pending_result->Error(error_code, error); } }; @@ -156,11 +179,13 @@ void CameraImpl::OnResumePreviewSucceeded() { } } -void CameraImpl::OnResumePreviewFailed(const std::string& error) { +void CameraImpl::OnResumePreviewFailed(CameraResult result, + const std::string& error) { auto pending_result = GetPendingResultByType(PendingResultType::kResumePreview); if (pending_result) { - pending_result->Error("camera_error", error); + std::string error_code = GetErrorCode(result); + pending_result->Error(error_code, error); } } @@ -172,11 +197,13 @@ void CameraImpl::OnPausePreviewSucceeded() { } } -void CameraImpl::OnPausePreviewFailed(const std::string& error) { +void CameraImpl::OnPausePreviewFailed(CameraResult result, + const std::string& error) { auto pending_result = GetPendingResultByType(PendingResultType::kPausePreview); if (pending_result) { - pending_result->Error("camera_error", error); + std::string error_code = GetErrorCode(result); + pending_result->Error(error_code, error); } } @@ -187,10 +214,12 @@ void CameraImpl::OnStartRecordSucceeded() { } }; -void CameraImpl::OnStartRecordFailed(const std::string& error) { +void CameraImpl::OnStartRecordFailed(CameraResult result, + const std::string& error) { auto pending_result = GetPendingResultByType(PendingResultType::kStartRecord); if (pending_result) { - pending_result->Error("camera_error", error); + std::string error_code = GetErrorCode(result); + pending_result->Error(error_code, error); } }; @@ -201,10 +230,12 @@ void CameraImpl::OnStopRecordSucceeded(const std::string& file_path) { } }; -void CameraImpl::OnStopRecordFailed(const std::string& error) { +void CameraImpl::OnStopRecordFailed(CameraResult result, + const std::string& error) { auto pending_result = GetPendingResultByType(PendingResultType::kStopRecord); if (pending_result) { - pending_result->Error("camera_error", error); + std::string error_code = GetErrorCode(result); + pending_result->Error(error_code, error); } }; @@ -215,11 +246,13 @@ void CameraImpl::OnTakePictureSucceeded(const std::string& file_path) { } }; -void CameraImpl::OnTakePictureFailed(const std::string& error) { +void CameraImpl::OnTakePictureFailed(CameraResult result, + const std::string& error) { auto pending_take_picture_result = GetPendingResultByType(PendingResultType::kTakePicture); if (pending_take_picture_result) { - pending_take_picture_result->Error("camera_error", error); + std::string error_code = GetErrorCode(result); + pending_take_picture_result->Error(error_code, error); } }; @@ -238,9 +271,10 @@ void CameraImpl::OnVideoRecordSucceeded(const std::string& file_path, } } -void CameraImpl::OnVideoRecordFailed(const std::string& error){}; +void CameraImpl::OnVideoRecordFailed(CameraResult result, + const std::string& error){}; -void CameraImpl::OnCaptureError(const std::string& error) { +void CameraImpl::OnCaptureError(CameraResult result, const std::string& error) { if (messenger_ && camera_id_ >= 0) { auto channel = GetMethodChannel(); @@ -250,7 +284,8 @@ void CameraImpl::OnCaptureError(const std::string& error) { channel->InvokeMethod(kErrorEvent, std::move(message_data)); } - SendErrorForPendingResults("capture_error", error); + std::string error_code = GetErrorCode(result); + SendErrorForPendingResults(error_code, error); } void CameraImpl::OnCameraClosing() { diff --git a/packages/camera/camera_windows/windows/camera.h b/packages/camera/camera_windows/windows/camera.h index 6996231c7ab4..8508da1924d0 100644 --- a/packages/camera/camera_windows/windows/camera.h +++ b/packages/camera/camera_windows/windows/camera.h @@ -63,7 +63,9 @@ class Camera : public CaptureControllerListener { virtual camera_windows::CaptureController* GetCaptureController() = 0; // Initializes this camera and its associated capture controller. - virtual void InitCamera(flutter::TextureRegistrar* texture_registrar, + // + // Returns false if initialization fails. + virtual bool InitCamera(flutter::TextureRegistrar* texture_registrar, flutter::BinaryMessenger* messenger, bool record_audio, ResolutionPreset resolution_preset) = 0; @@ -85,23 +87,31 @@ class CameraImpl : public Camera { // CaptureControllerListener void OnCreateCaptureEngineSucceeded(int64_t texture_id) override; - void OnCreateCaptureEngineFailed(const std::string& error) override; + void OnCreateCaptureEngineFailed(CameraResult result, + const std::string& error) override; void OnStartPreviewSucceeded(int32_t width, int32_t height) override; - void OnStartPreviewFailed(const std::string& error) override; + void OnStartPreviewFailed(CameraResult result, + const std::string& error) override; void OnPausePreviewSucceeded() override; - void OnPausePreviewFailed(const std::string& error) override; + void OnPausePreviewFailed(CameraResult result, + const std::string& error) override; void OnResumePreviewSucceeded() override; - void OnResumePreviewFailed(const std::string& error) override; + void OnResumePreviewFailed(CameraResult result, + const std::string& error) override; void OnStartRecordSucceeded() override; - void OnStartRecordFailed(const std::string& error) override; + void OnStartRecordFailed(CameraResult result, + const std::string& error) override; void OnStopRecordSucceeded(const std::string& file_path) override; - void OnStopRecordFailed(const std::string& error) override; + void OnStopRecordFailed(CameraResult result, + const std::string& error) override; void OnTakePictureSucceeded(const std::string& file_path) override; - void OnTakePictureFailed(const std::string& error) override; + void OnTakePictureFailed(CameraResult result, + const std::string& error) override; void OnVideoRecordSucceeded(const std::string& file_path, int64_t video_duration) override; - void OnVideoRecordFailed(const std::string& error) override; - void OnCaptureError(const std::string& error) override; + void OnVideoRecordFailed(CameraResult result, + const std::string& error) override; + void OnCaptureError(CameraResult result, const std::string& error) override; // Camera bool HasDeviceId(std::string& device_id) const override { @@ -116,7 +126,7 @@ class CameraImpl : public Camera { camera_windows::CaptureController* GetCaptureController() override { return capture_controller_.get(); } - void InitCamera(flutter::TextureRegistrar* texture_registrar, + bool InitCamera(flutter::TextureRegistrar* texture_registrar, flutter::BinaryMessenger* messenger, bool record_audio, ResolutionPreset resolution_preset) override; @@ -124,7 +134,9 @@ class CameraImpl : public Camera { // // This is a convenience method called by |InitCamera| but also used in // tests. - void InitCamera( + // + // Returns false if initialization fails. + bool InitCamera( std::unique_ptr capture_controller_factory, flutter::TextureRegistrar* texture_registrar, flutter::BinaryMessenger* messenger, bool record_audio, @@ -135,9 +147,9 @@ class CameraImpl : public Camera { // error ID and description. Pending results are cleared in the process. // // error_code: A string error code describing the error. - // error_message: A user-readable error message (optional). + // description: A user-readable error message (optional). void SendErrorForPendingResults(const std::string& error_code, - const std::string& descripion); + const std::string& description); // Called when camera is disposed. // Sends camera closing message to the cameras method channel. diff --git a/packages/camera/camera_windows/windows/camera_plugin.cpp b/packages/camera/camera_windows/windows/camera_plugin.cpp index 3b795e02047a..5503d17e702b 100644 --- a/packages/camera/camera_windows/windows/camera_plugin.cpp +++ b/packages/camera/camera_windows/windows/camera_plugin.cpp @@ -398,9 +398,11 @@ void CameraPlugin::CreateMethodHandler( resolution_preset = ResolutionPreset::kAuto; } - camera->InitCamera(texture_registrar_, messenger_, *record_audio, - resolution_preset); - cameras_.push_back(std::move(camera)); + bool initialized = camera->InitCamera(texture_registrar_, messenger_, + *record_audio, resolution_preset); + if (initialized) { + cameras_.push_back(std::move(camera)); + } } } diff --git a/packages/camera/camera_windows/windows/capture_controller.cpp b/packages/camera/camera_windows/windows/capture_controller.cpp index 084b03640bef..384c86ac109b 100644 --- a/packages/camera/camera_windows/windows/capture_controller.cpp +++ b/packages/camera/camera_windows/windows/capture_controller.cpp @@ -22,6 +22,15 @@ namespace camera_windows { using Microsoft::WRL::ComPtr; +CameraResult GetCameraResult(HRESULT hr) { + if (SUCCEEDED(hr)) { + return CameraResult::kSuccess; + } + + return hr == E_ACCESSDENIED ? CameraResult::kAccessDenied + : CameraResult::kError; +} + CaptureControllerImpl::CaptureControllerImpl( CaptureControllerListener* listener) : capture_controller_listener_(listener), CaptureController(){}; @@ -237,6 +246,8 @@ HRESULT CaptureControllerImpl::CreateCaptureEngine() { return hr; } + // Check MF_CAPTURE_ENGINE_INITIALIZED event handling + // for response process. hr = capture_engine_->Initialize(capture_engine_callback_handler_.Get(), attributes.Get(), audio_source_.Get(), video_source_.Get()); @@ -244,7 +255,7 @@ HRESULT CaptureControllerImpl::CreateCaptureEngine() { } void CaptureControllerImpl::ResetCaptureController() { - if (record_handler_) { + if (record_handler_ && record_handler_->CanStop()) { if (record_handler_->IsContinuousRecording()) { StopRecord(); } else if (record_handler_->IsTimedRecording()) { @@ -288,17 +299,19 @@ void CaptureControllerImpl::ResetCaptureController() { texture_handler_ = nullptr; } -void CaptureControllerImpl::InitCaptureDevice( +bool CaptureControllerImpl::InitCaptureDevice( flutter::TextureRegistrar* texture_registrar, const std::string& device_id, bool record_audio, ResolutionPreset resolution_preset) { assert(capture_controller_listener_); if (IsInitialized()) { - return capture_controller_listener_->OnCreateCaptureEngineFailed( - "Capture device already initialized"); + capture_controller_listener_->OnCreateCaptureEngineFailed( + CameraResult::kError, "Capture device already initialized"); + return false; } else if (capture_engine_state_ == CaptureEngineState::kInitializing) { - return capture_controller_listener_->OnCreateCaptureEngineFailed( - "Capture device already initializing"); + capture_controller_listener_->OnCreateCaptureEngineFailed( + CameraResult::kError, "Capture device already initializing"); + return false; } capture_engine_state_ = CaptureEngineState::kInitializing; @@ -313,9 +326,9 @@ void CaptureControllerImpl::InitCaptureDevice( if (FAILED(hr)) { capture_controller_listener_->OnCreateCaptureEngineFailed( - "Failed to create camera"); + GetCameraResult(hr), "Failed to create camera"); ResetCaptureController(); - return; + return false; } media_foundation_started_ = true; @@ -324,10 +337,12 @@ void CaptureControllerImpl::InitCaptureDevice( HRESULT hr = CreateCaptureEngine(); if (FAILED(hr)) { capture_controller_listener_->OnCreateCaptureEngineFailed( - "Failed to create camera"); + GetCameraResult(hr), "Failed to create camera"); ResetCaptureController(); - return; + return false; } + + return true; } void CaptureControllerImpl::TakePicture(const std::string& file_path) { @@ -335,29 +350,34 @@ void CaptureControllerImpl::TakePicture(const std::string& file_path) { assert(capture_engine_); if (!IsInitialized()) { - return OnPicture(false, "Not initialized"); + return OnPicture(CameraResult::kError, "Not initialized"); } + HRESULT hr = S_OK; + if (!base_capture_media_type_) { // Enumerates mediatypes and finds media type for video capture. - if (FAILED(FindBaseMediaTypes())) { - return OnPicture(false, "Failed to initialize photo capture"); + hr = FindBaseMediaTypes(); + if (FAILED(hr)) { + return OnPicture(GetCameraResult(hr), + "Failed to initialize photo capture"); } } if (!photo_handler_) { photo_handler_ = std::make_unique(); } else if (photo_handler_->IsTakingPhoto()) { - return OnPicture(false, "Photo already requested"); + return OnPicture(CameraResult::kError, "Photo already requested"); } // Check MF_CAPTURE_ENGINE_PHOTO_TAKEN event handling // for response process. - if (!photo_handler_->TakePhoto(file_path, capture_engine_.Get(), - base_capture_media_type_.Get())) { + hr = photo_handler_->TakePhoto(file_path, capture_engine_.Get(), + base_capture_media_type_.Get()); + if (FAILED(hr)) { // Destroy photo handler on error cases to make sure state is resetted. photo_handler_ = nullptr; - return OnPicture(false, "Failed to take photo"); + return OnPicture(GetCameraResult(hr), "Failed to take photo"); } } @@ -387,7 +407,7 @@ uint32_t CaptureControllerImpl::GetMaxPreviewHeight() const { } } -// Finds best mediat type for given source stream index and max height; +// Finds best media type for given source stream index and max height; bool FindBestMediaType(DWORD source_stream_index, IMFCaptureSource* source, IMFMediaType** target_media_type, uint32_t max_height, uint32_t* target_frame_width, @@ -481,15 +501,19 @@ void CaptureControllerImpl::StartRecord(const std::string& file_path, assert(capture_engine_); if (!IsInitialized()) { - return OnRecordStarted(false, + return OnRecordStarted(CameraResult::kError, "Camera not initialized. Camera should be " "disposed and reinitialized."); } + HRESULT hr = S_OK; + if (!base_capture_media_type_) { // Enumerates mediatypes and finds media type for video capture. - if (FAILED(FindBaseMediaTypes())) { - return OnRecordStarted(false, "Failed to initialize video recording"); + hr = FindBaseMediaTypes(); + if (FAILED(hr)) { + return OnRecordStarted(GetCameraResult(hr), + "Failed to initialize video recording"); } } @@ -497,19 +521,21 @@ void CaptureControllerImpl::StartRecord(const std::string& file_path, record_handler_ = std::make_unique(record_audio_); } else if (!record_handler_->CanStart()) { return OnRecordStarted( - false, + CameraResult::kError, "Recording cannot be started. Previous recording must be stopped " "first."); } // Check MF_CAPTURE_ENGINE_RECORD_STARTED event handling for response // process. - if (!record_handler_->StartRecord(file_path, max_video_duration_ms, + hr = record_handler_->StartRecord(file_path, max_video_duration_ms, capture_engine_.Get(), - base_capture_media_type_.Get())) { + base_capture_media_type_.Get()); + if (FAILED(hr)) { // Destroy record handler on error cases to make sure state is resetted. record_handler_ = nullptr; - return OnRecordStarted(false, "Failed to start video recording"); + return OnRecordStarted(GetCameraResult(hr), + "Failed to start video recording"); } } @@ -517,21 +543,22 @@ void CaptureControllerImpl::StopRecord() { assert(capture_controller_listener_); if (!IsInitialized()) { - return OnRecordStopped(false, + return OnRecordStopped(CameraResult::kError, "Camera not initialized. Camera should be " "disposed and reinitialized."); } if (!record_handler_ && !record_handler_->CanStop()) { - return OnRecordStopped(false, "Recording cannot be stopped."); + return OnRecordStopped(CameraResult::kError, + "Recording cannot be stopped."); } // Check MF_CAPTURE_ENGINE_RECORD_STOPPED event handling for response // process. - if (!record_handler_->StopRecord(capture_engine_.Get())) { - // Destroy record handler on error cases to make sure state is resetted. - record_handler_ = nullptr; - return OnRecordStopped(false, "Failed to stop video recording"); + HRESULT hr = record_handler_->StopRecord(capture_engine_.Get()); + if (FAILED(hr)) { + return OnRecordStopped(GetCameraResult(hr), + "Failed to stop video recording"); } } @@ -543,11 +570,12 @@ void CaptureControllerImpl::StopTimedRecord() { return; } - if (!record_handler_->StopRecord(capture_engine_.Get())) { + HRESULT hr = record_handler_->StopRecord(capture_engine_.Get()); + if (FAILED(hr)) { // Destroy record handler on error cases to make sure state is resetted. record_handler_ = nullptr; return capture_controller_listener_->OnVideoRecordFailed( - "Failed to record video"); + GetCameraResult(hr), "Failed to record video"); } } @@ -559,37 +587,46 @@ void CaptureControllerImpl::StartPreview() { assert(texture_handler_); if (!IsInitialized() || !texture_handler_) { - return OnPreviewStarted(false, + return OnPreviewStarted(CameraResult::kError, "Camera not initialized. Camera should be " "disposed and reinitialized."); } + HRESULT hr = S_OK; + if (!base_preview_media_type_) { // Enumerates mediatypes and finds media type for video capture. - if (FAILED(FindBaseMediaTypes())) { - return OnPreviewStarted(false, "Failed to initialize video preview"); + hr = FindBaseMediaTypes(); + if (FAILED(hr)) { + return OnPreviewStarted(GetCameraResult(hr), + "Failed to initialize video preview"); } } texture_handler_->UpdateTextureSize(preview_frame_width_, preview_frame_height_); + // TODO(loic-sharma): This does not handle duplicate calls properly. + // See: https://github.com/flutter/flutter/issues/108404 if (!preview_handler_) { preview_handler_ = std::make_unique(); } else if (preview_handler_->IsInitialized()) { - return OnPreviewStarted(true, ""); + return OnPreviewStarted(CameraResult::kSuccess, ""); } else { - return OnPreviewStarted(false, "Preview already exists"); + return OnPreviewStarted(CameraResult::kError, "Preview already exists"); } // Check MF_CAPTURE_ENGINE_PREVIEW_STARTED event handling for response // process. - if (!preview_handler_->StartPreview(capture_engine_.Get(), + hr = preview_handler_->StartPreview(capture_engine_.Get(), base_preview_media_type_.Get(), - capture_engine_callback_handler_.Get())) { + capture_engine_callback_handler_.Get()); + + if (FAILED(hr)) { // Destroy preview handler on error cases to make sure state is resetted. preview_handler_ = nullptr; - return OnPreviewStarted(false, "Failed to start video preview"); + return OnPreviewStarted(GetCameraResult(hr), + "Failed to start video preview"); } } @@ -598,15 +635,15 @@ void CaptureControllerImpl::StartPreview() { // pausing and resuming the preview. // Check MF_CAPTURE_ENGINE_PREVIEW_STOPPED event handling for response // process. -void CaptureControllerImpl::StopPreview() { +HRESULT CaptureControllerImpl::StopPreview() { assert(capture_engine_); - if (!IsInitialized() && !preview_handler_) { - return; + if (!IsInitialized() || !preview_handler_) { + return S_OK; } // Requests to stop preview. - preview_handler_->StopPreview(capture_engine_.Get()); + return preview_handler_->StopPreview(capture_engine_.Get()); } // Marks preview as paused. @@ -615,16 +652,16 @@ void CaptureControllerImpl::StopPreview() { void CaptureControllerImpl::PausePreview() { assert(capture_controller_listener_); - if (!preview_handler_ && !preview_handler_->IsInitialized()) { + if (!preview_handler_ || !preview_handler_->IsInitialized()) { return capture_controller_listener_->OnPausePreviewFailed( - "Preview not started"); + CameraResult::kError, "Preview not started"); } if (preview_handler_->PausePreview()) { capture_controller_listener_->OnPausePreviewSucceeded(); } else { capture_controller_listener_->OnPausePreviewFailed( - "Failed to pause preview"); + CameraResult::kError, "Failed to pause preview"); } } @@ -634,16 +671,16 @@ void CaptureControllerImpl::PausePreview() { void CaptureControllerImpl::ResumePreview() { assert(capture_controller_listener_); - if (!preview_handler_ && !preview_handler_->IsInitialized()) { + if (!preview_handler_ || !preview_handler_->IsInitialized()) { return capture_controller_listener_->OnResumePreviewFailed( - "Preview not started"); + CameraResult::kError, "Preview not started"); } if (preview_handler_->ResumePreview()) { capture_controller_listener_->OnResumePreviewSucceeded(); } else { capture_controller_listener_->OnResumePreviewFailed( - "Failed to pause preview"); + CameraResult::kError, "Failed to pause preview"); } } @@ -671,22 +708,23 @@ void CaptureControllerImpl::OnEvent(IMFMediaEvent* event) { error = Utf8FromUtf16(err.ErrorMessage()); } + CameraResult event_result = GetCameraResult(event_hr); if (extended_type_guid == MF_CAPTURE_ENGINE_ERROR) { - OnCaptureEngineError(event_hr, error); + OnCaptureEngineError(event_result, error); } else if (extended_type_guid == MF_CAPTURE_ENGINE_INITIALIZED) { - OnCaptureEngineInitialized(SUCCEEDED(event_hr), error); + OnCaptureEngineInitialized(event_result, error); } else if (extended_type_guid == MF_CAPTURE_ENGINE_PREVIEW_STARTED) { // Preview is marked as started after first frame is captured. // This is because, CaptureEngine might inform that preview is started // even if error is thrown right after. } else if (extended_type_guid == MF_CAPTURE_ENGINE_PREVIEW_STOPPED) { - OnPreviewStopped(SUCCEEDED(event_hr), error); + OnPreviewStopped(event_result, error); } else if (extended_type_guid == MF_CAPTURE_ENGINE_RECORD_STARTED) { - OnRecordStarted(SUCCEEDED(event_hr), error); + OnRecordStarted(event_result, error); } else if (extended_type_guid == MF_CAPTURE_ENGINE_RECORD_STOPPED) { - OnRecordStopped(SUCCEEDED(event_hr), error); + OnRecordStopped(event_result, error); } else if (extended_type_guid == MF_CAPTURE_ENGINE_PHOTO_TAKEN) { - OnPicture(SUCCEEDED(event_hr), error); + OnPicture(event_result, error); } else if (extended_type_guid == MF_CAPTURE_ENGINE_CAMERA_STREAM_BLOCKED) { // TODO: Inform capture state to flutter. } else if (extended_type_guid == @@ -697,8 +735,9 @@ void CaptureControllerImpl::OnEvent(IMFMediaEvent* event) { } // Handles Picture event and informs CaptureControllerListener. -void CaptureControllerImpl::OnPicture(bool success, const std::string& error) { - if (success && photo_handler_) { +void CaptureControllerImpl::OnPicture(CameraResult result, + const std::string& error) { + if (result == CameraResult::kSuccess && photo_handler_) { if (capture_controller_listener_) { std::string path = photo_handler_->GetPhotoPath(); capture_controller_listener_->OnTakePictureSucceeded(path); @@ -706,7 +745,7 @@ void CaptureControllerImpl::OnPicture(bool success, const std::string& error) { photo_handler_->OnPhotoTaken(); } else { if (capture_controller_listener_) { - capture_controller_listener_->OnTakePictureFailed(error); + capture_controller_listener_->OnTakePictureFailed(result, error); } // Destroy photo handler on error cases to make sure state is resetted. photo_handler_ = nullptr; @@ -716,8 +755,15 @@ void CaptureControllerImpl::OnPicture(bool success, const std::string& error) { // Handles CaptureEngineInitialized event and informs // CaptureControllerListener. void CaptureControllerImpl::OnCaptureEngineInitialized( - bool success, const std::string& error) { + CameraResult result, const std::string& error) { if (capture_controller_listener_) { + if (result != CameraResult::kSuccess) { + capture_controller_listener_->OnCreateCaptureEngineFailed( + result, "Failed to initialize capture engine"); + ResetCaptureController(); + return; + } + // Create texture handler and register new texture. texture_handler_ = std::make_unique(texture_registrar_); @@ -727,7 +773,7 @@ void CaptureControllerImpl::OnCaptureEngineInitialized( capture_engine_state_ = CaptureEngineState::kInitialized; } else { capture_controller_listener_->OnCreateCaptureEngineFailed( - "Failed to create texture_id"); + CameraResult::kError, "Failed to create texture_id"); // Reset state ResetCaptureController(); } @@ -735,10 +781,10 @@ void CaptureControllerImpl::OnCaptureEngineInitialized( } // Handles CaptureEngineError event and informs CaptureControllerListener. -void CaptureControllerImpl::OnCaptureEngineError(HRESULT hr, +void CaptureControllerImpl::OnCaptureEngineError(CameraResult result, const std::string& error) { if (capture_controller_listener_) { - capture_controller_listener_->OnCaptureError(error); + capture_controller_listener_->OnCaptureError(result, error); } // TODO: If MF_CAPTURE_ENGINE_ERROR is returned, @@ -748,9 +794,9 @@ void CaptureControllerImpl::OnCaptureEngineError(HRESULT hr, // Handles PreviewStarted event and informs CaptureControllerListener. // This should be called only after first frame has been received or // in error cases. -void CaptureControllerImpl::OnPreviewStarted(bool success, +void CaptureControllerImpl::OnPreviewStarted(CameraResult result, const std::string& error) { - if (preview_handler_ && success) { + if (preview_handler_ && result == CameraResult::kSuccess) { preview_handler_->OnPreviewStarted(); } else { // Destroy preview handler on error cases to make sure state is resetted. @@ -758,17 +804,18 @@ void CaptureControllerImpl::OnPreviewStarted(bool success, } if (capture_controller_listener_) { - if (success && preview_frame_width_ > 0 && preview_frame_height_ > 0) { + if (result == CameraResult::kSuccess && preview_frame_width_ > 0 && + preview_frame_height_ > 0) { capture_controller_listener_->OnStartPreviewSucceeded( preview_frame_width_, preview_frame_height_); } else { - capture_controller_listener_->OnStartPreviewFailed(error); + capture_controller_listener_->OnStartPreviewFailed(result, error); } } }; // Handles PreviewStopped event. -void CaptureControllerImpl::OnPreviewStopped(bool success, +void CaptureControllerImpl::OnPreviewStopped(CameraResult result, const std::string& error) { // Preview handler is destroyed if preview is stopped as it // does not have any use anymore. @@ -776,16 +823,16 @@ void CaptureControllerImpl::OnPreviewStopped(bool success, }; // Handles RecordStarted event and informs CaptureControllerListener. -void CaptureControllerImpl::OnRecordStarted(bool success, +void CaptureControllerImpl::OnRecordStarted(CameraResult result, const std::string& error) { - if (success && record_handler_) { + if (result == CameraResult::kSuccess && record_handler_) { record_handler_->OnRecordStarted(); if (capture_controller_listener_) { capture_controller_listener_->OnStartRecordSucceeded(); } } else { if (capture_controller_listener_) { - capture_controller_listener_->OnStartRecordFailed(error); + capture_controller_listener_->OnStartRecordFailed(result, error); } // Destroy record handler on error cases to make sure state is resetted. @@ -794,13 +841,13 @@ void CaptureControllerImpl::OnRecordStarted(bool success, }; // Handles RecordStopped event and informs CaptureControllerListener. -void CaptureControllerImpl::OnRecordStopped(bool success, +void CaptureControllerImpl::OnRecordStopped(CameraResult result, const std::string& error) { if (capture_controller_listener_ && record_handler_) { // Always calls OnStopRecord listener methods // to handle separate stop record request for timed records. - if (success) { + if (result == CameraResult::kSuccess) { std::string path = record_handler_->GetRecordPath(); capture_controller_listener_->OnStopRecordSucceeded(path); if (record_handler_->IsTimedRecording()) { @@ -808,14 +855,14 @@ void CaptureControllerImpl::OnRecordStopped(bool success, path, (record_handler_->GetRecordedDuration() / 1000)); } } else { - capture_controller_listener_->OnStopRecordFailed(error); + capture_controller_listener_->OnStopRecordFailed(result, error); if (record_handler_->IsTimedRecording()) { - capture_controller_listener_->OnVideoRecordFailed(error); + capture_controller_listener_->OnVideoRecordFailed(result, error); } } } - if (success && record_handler_) { + if (result == CameraResult::kSuccess && record_handler_) { record_handler_->OnRecordStopped(); } else { // Destroy record handler on error cases to make sure state is resetted. @@ -844,9 +891,9 @@ void CaptureControllerImpl::UpdateCaptureTime(uint64_t capture_time_us) { } if (preview_handler_ && preview_handler_->IsStarting()) { - // Informs that first frame is captured succeffully and preview has + // Informs that first frame is captured successfully and preview has // started. - OnPreviewStarted(true, ""); + OnPreviewStarted(CameraResult::kSuccess, ""); } // Checks if max_video_duration_ms is passed. diff --git a/packages/camera/camera_windows/windows/capture_controller.h b/packages/camera/camera_windows/windows/capture_controller.h index 34e378109d8f..9536be70c50a 100644 --- a/packages/camera/camera_windows/windows/capture_controller.h +++ b/packages/camera/camera_windows/windows/capture_controller.h @@ -75,6 +75,9 @@ class CaptureController { // Initializes the capture controller with the specified device id. // + // Returns false if the capture controller could not be initialized + // or is already initialized. + // // texture_registrar: Pointer to Flutter TextureRegistrar instance. Used to // register texture for capture preview. // device_id: A string that holds information of camera device id to @@ -82,7 +85,7 @@ class CaptureController { // record_audio: A boolean value telling if audio should be captured on // video recording. // resolution_preset: Maximum capture resolution height. - virtual void InitCaptureDevice(TextureRegistrar* texture_registrar, + virtual bool InitCaptureDevice(TextureRegistrar* texture_registrar, const std::string& device_id, bool record_audio, ResolutionPreset resolution_preset) = 0; @@ -132,7 +135,7 @@ class CaptureControllerImpl : public CaptureController, CaptureControllerImpl& operator=(const CaptureControllerImpl&) = delete; // CaptureController - void InitCaptureDevice(TextureRegistrar* texture_registrar, + bool InitCaptureDevice(TextureRegistrar* texture_registrar, const std::string& device_id, bool record_audio, ResolutionPreset resolution_preset) override; uint32_t GetPreviewWidth() const override { return preview_frame_width_; } @@ -204,28 +207,29 @@ class CaptureControllerImpl : public CaptureController, void StopTimedRecord(); // Stops preview. Called internally on camera reset and dispose. - void StopPreview(); + HRESULT StopPreview(); // Handles capture engine initalization event. - void OnCaptureEngineInitialized(bool success, const std::string& error); + void OnCaptureEngineInitialized(CameraResult result, + const std::string& error); // Handles capture engine errors. - void OnCaptureEngineError(HRESULT hr, const std::string& error); + void OnCaptureEngineError(CameraResult result, const std::string& error); // Handles picture events. - void OnPicture(bool success, const std::string& error); + void OnPicture(CameraResult result, const std::string& error); // Handles preview started events. - void OnPreviewStarted(bool success, const std::string& error); + void OnPreviewStarted(CameraResult result, const std::string& error); // Handles preview stopped events. - void OnPreviewStopped(bool success, const std::string& error); + void OnPreviewStopped(CameraResult result, const std::string& error); // Handles record started events. - void OnRecordStarted(bool success, const std::string& error); + void OnRecordStarted(CameraResult result, const std::string& error); // Handles record stopped events. - void OnRecordStopped(bool success, const std::string& error); + void OnRecordStopped(CameraResult result, const std::string& error); bool media_foundation_started_ = false; bool record_audio_ = false; diff --git a/packages/camera/camera_windows/windows/capture_controller_listener.h b/packages/camera/camera_windows/windows/capture_controller_listener.h index 0e713ea7af18..bc7a173925a8 100644 --- a/packages/camera/camera_windows/windows/capture_controller_listener.h +++ b/packages/camera/camera_windows/windows/capture_controller_listener.h @@ -9,6 +9,18 @@ namespace camera_windows { +// Results that can occur when interacting with the camera. +enum class CameraResult { + // Camera operation succeeded. + kSuccess, + + // Camera operation failed. + kError, + + // Camera access permission is denied. + kAccessDenied, +}; + // Interface for classes that receives callbacks on events from the associated // |CaptureController|. class CaptureControllerListener { @@ -22,8 +34,10 @@ class CaptureControllerListener { // Called by CaptureController if initializing the capture engine fails. // + // result: The kind of result. // error: A string describing the error. - virtual void OnCreateCaptureEngineFailed(const std::string& error) = 0; + virtual void OnCreateCaptureEngineFailed(CameraResult result, + const std::string& error) = 0; // Called by CaptureController on successfully started preview. // @@ -33,32 +47,40 @@ class CaptureControllerListener { // Called by CaptureController if starting the preview fails. // + // result: The kind of result. // error: A string describing the error. - virtual void OnStartPreviewFailed(const std::string& error) = 0; + virtual void OnStartPreviewFailed(CameraResult result, + const std::string& error) = 0; // Called by CaptureController on successfully paused preview. virtual void OnPausePreviewSucceeded() = 0; // Called by CaptureController if pausing the preview fails. // + // result: The kind of result. // error: A string describing the error. - virtual void OnPausePreviewFailed(const std::string& error) = 0; + virtual void OnPausePreviewFailed(CameraResult result, + const std::string& error) = 0; // Called by CaptureController on successfully resumed preview. virtual void OnResumePreviewSucceeded() = 0; // Called by CaptureController if resuming the preview fails. // + // result: The kind of result. // error: A string describing the error. - virtual void OnResumePreviewFailed(const std::string& error) = 0; + virtual void OnResumePreviewFailed(CameraResult result, + const std::string& error) = 0; // Called by CaptureController on successfully started recording. virtual void OnStartRecordSucceeded() = 0; // Called by CaptureController if starting the recording fails. // + // result: The kind of result. // error: A string describing the error. - virtual void OnStartRecordFailed(const std::string& error) = 0; + virtual void OnStartRecordFailed(CameraResult result, + const std::string& error) = 0; // Called by CaptureController on successfully stopped recording. // @@ -67,8 +89,10 @@ class CaptureControllerListener { // Called by CaptureController if stopping the recording fails. // + // result: The kind of result. // error: A string describing the error. - virtual void OnStopRecordFailed(const std::string& error) = 0; + virtual void OnStopRecordFailed(CameraResult result, + const std::string& error) = 0; // Called by CaptureController on successfully captured picture. // @@ -77,8 +101,10 @@ class CaptureControllerListener { // Called by CaptureController if taking picture fails. // + // result: The kind of result. // error: A string describing the error. - virtual void OnTakePictureFailed(const std::string& error) = 0; + virtual void OnTakePictureFailed(CameraResult result, + const std::string& error) = 0; // Called by CaptureController when timed recording is successfully recorded. // @@ -89,14 +115,18 @@ class CaptureControllerListener { // Called by CaptureController if timed recording fails. // + // result: The kind of result. // error: A string describing the error. - virtual void OnVideoRecordFailed(const std::string& error) = 0; + virtual void OnVideoRecordFailed(CameraResult result, + const std::string& error) = 0; // Called by CaptureController if capture engine returns error. // For example when camera is disconnected while on use. // + // result: The kind of result. // error: A string describing the error. - virtual void OnCaptureError(const std::string& error) = 0; + virtual void OnCaptureError(CameraResult result, + const std::string& error) = 0; }; } // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/photo_handler.cpp b/packages/camera/camera_windows/windows/photo_handler.cpp index 10df230c2cf2..479f0d3c5ac2 100644 --- a/packages/camera/camera_windows/windows/photo_handler.cpp +++ b/packages/camera/camera_windows/windows/photo_handler.cpp @@ -116,21 +116,23 @@ HRESULT PhotoHandler::InitPhotoSink(IMFCaptureEngine* capture_engine, return hr; } -bool PhotoHandler::TakePhoto(const std::string& file_path, - IMFCaptureEngine* capture_engine, - IMFMediaType* base_media_type) { +HRESULT PhotoHandler::TakePhoto(const std::string& file_path, + IMFCaptureEngine* capture_engine, + IMFMediaType* base_media_type) { assert(!file_path.empty()); assert(capture_engine); assert(base_media_type); file_path_ = file_path; - if (FAILED(InitPhotoSink(capture_engine, base_media_type))) { - return false; + HRESULT hr = InitPhotoSink(capture_engine, base_media_type); + if (FAILED(hr)) { + return hr; } photo_state_ = PhotoState::kTakingPhoto; - return SUCCEEDED(capture_engine->TakePhoto()); + + return capture_engine->TakePhoto(); } void PhotoHandler::OnPhotoTaken() { diff --git a/packages/camera/camera_windows/windows/photo_handler.h b/packages/camera/camera_windows/windows/photo_handler.h index ef0d98bfc45f..4d6ddf1a55b8 100644 --- a/packages/camera/camera_windows/windows/photo_handler.h +++ b/packages/camera/camera_windows/windows/photo_handler.h @@ -41,17 +41,17 @@ class PhotoHandler { // to take photo. // // Sets photo state to: kTakingPhoto. - // Returns false if photo cannot be taken. // // capture_engine: A pointer to capture engine instance. // Called to take the photo. // base_media_type: A pointer to base media type used as a base // for the actual photo capture media type. // file_path: A string that hold file path for photo capture. - bool TakePhoto(const std::string& file_path, IMFCaptureEngine* capture_engine, - IMFMediaType* base_media_type); + HRESULT TakePhoto(const std::string& file_path, + IMFCaptureEngine* capture_engine, + IMFMediaType* base_media_type); - // Set the photo handler recording state to: kIdel. + // Set the photo handler recording state to: kIdle. void OnPhotoTaken(); // Returns true if photo state is kIdle. diff --git a/packages/camera/camera_windows/windows/preview_handler.cpp b/packages/camera/camera_windows/windows/preview_handler.cpp index d7fb2721259c..538754c3e9e2 100644 --- a/packages/camera/camera_windows/windows/preview_handler.cpp +++ b/packages/camera/camera_windows/windows/preview_handler.cpp @@ -113,29 +113,31 @@ HRESULT PreviewHandler::InitPreviewSink( return hr; } -bool PreviewHandler::StartPreview(IMFCaptureEngine* capture_engine, - IMFMediaType* base_media_type, - CaptureEngineListener* sample_callback) { +HRESULT PreviewHandler::StartPreview(IMFCaptureEngine* capture_engine, + IMFMediaType* base_media_type, + CaptureEngineListener* sample_callback) { assert(capture_engine); assert(base_media_type); - if (FAILED( - InitPreviewSink(capture_engine, base_media_type, sample_callback))) { - return false; + HRESULT hr = + InitPreviewSink(capture_engine, base_media_type, sample_callback); + + if (FAILED(hr)) { + return hr; } preview_state_ = PreviewState::kStarting; - return SUCCEEDED(capture_engine->StartPreview()); + return capture_engine->StartPreview(); } -bool PreviewHandler::StopPreview(IMFCaptureEngine* capture_engine) { +HRESULT PreviewHandler::StopPreview(IMFCaptureEngine* capture_engine) { if (preview_state_ == PreviewState::kStarting || preview_state_ == PreviewState::kRunning || preview_state_ == PreviewState::kPaused) { preview_state_ = PreviewState::kStopping; - return SUCCEEDED(capture_engine->StopPreview()); + return capture_engine->StopPreview(); } - return false; + return E_FAIL; } bool PreviewHandler::PausePreview() { diff --git a/packages/camera/camera_windows/windows/preview_handler.h b/packages/camera/camera_windows/windows/preview_handler.h index 97b85fc28568..311cf5a76c2f 100644 --- a/packages/camera/camera_windows/windows/preview_handler.h +++ b/packages/camera/camera_windows/windows/preview_handler.h @@ -45,7 +45,6 @@ class PreviewHandler { // Initializes preview sink and requests capture engine to start previewing. // Sets preview state to: starting. - // Returns false if recording cannot be started. // // capture_engine: A pointer to capture engine instance. Used to start // the actual recording. @@ -53,16 +52,15 @@ class PreviewHandler { // for the actual video capture media type. // sample_callback: A pointer to capture engine listener. // This is set as sample callback for preview sink. - bool StartPreview(IMFCaptureEngine* capture_engine, - IMFMediaType* base_media_type, - CaptureEngineListener* sample_callback); + HRESULT StartPreview(IMFCaptureEngine* capture_engine, + IMFMediaType* base_media_type, + CaptureEngineListener* sample_callback); // Stops existing recording. - // Returns false if recording cannot be stopped. // // capture_engine: A pointer to capture engine instance. Used to stop // the ongoing recording. - bool StopPreview(IMFCaptureEngine* capture_engine); + HRESULT StopPreview(IMFCaptureEngine* capture_engine); // Set the preview handler recording state to: paused. bool PausePreview(); @@ -75,7 +73,7 @@ class PreviewHandler { // Returns true if preview state is running or paused. bool IsInitialized() const { - return preview_state_ == PreviewState::kRunning && + return preview_state_ == PreviewState::kRunning || preview_state_ == PreviewState::kPaused; } diff --git a/packages/camera/camera_windows/windows/record_handler.cpp b/packages/camera/camera_windows/windows/record_handler.cpp index 1cb258e162a5..0f7192533fdd 100644 --- a/packages/camera/camera_windows/windows/record_handler.cpp +++ b/packages/camera/camera_windows/windows/record_handler.cpp @@ -192,10 +192,10 @@ HRESULT RecordHandler::InitRecordSink(IMFCaptureEngine* capture_engine, return hr; } -bool RecordHandler::StartRecord(const std::string& file_path, - int64_t max_duration, - IMFCaptureEngine* capture_engine, - IMFMediaType* base_media_type) { +HRESULT RecordHandler::StartRecord(const std::string& file_path, + int64_t max_duration, + IMFCaptureEngine* capture_engine, + IMFMediaType* base_media_type) { assert(!file_path.empty()); assert(capture_engine); assert(base_media_type); @@ -206,23 +206,21 @@ bool RecordHandler::StartRecord(const std::string& file_path, recording_start_timestamp_us_ = -1; recording_duration_us_ = 0; - if (FAILED(InitRecordSink(capture_engine, base_media_type))) { - return false; + HRESULT hr = InitRecordSink(capture_engine, base_media_type); + if (FAILED(hr)) { + return hr; } recording_state_ = RecordState::kStarting; - capture_engine->StartRecord(); - - return true; + return capture_engine->StartRecord(); } -bool RecordHandler::StopRecord(IMFCaptureEngine* capture_engine) { +HRESULT RecordHandler::StopRecord(IMFCaptureEngine* capture_engine) { if (recording_state_ == RecordState::kRunning) { recording_state_ = RecordState::kStopping; - HRESULT hr = capture_engine->StopRecord(true, false); - return SUCCEEDED(hr); + return capture_engine->StopRecord(true, false); } - return false; + return E_FAIL; } void RecordHandler::OnRecordStarted() { @@ -238,6 +236,7 @@ void RecordHandler::OnRecordStopped() { recording_duration_us_ = 0; max_video_duration_ms_ = -1; recording_state_ = RecordState::kNotStarted; + type_ = RecordingType::kNone; } } diff --git a/packages/camera/camera_windows/windows/record_handler.h b/packages/camera/camera_windows/windows/record_handler.h index 0daa7f6546a1..0c87bf9cec64 100644 --- a/packages/camera/camera_windows/windows/record_handler.h +++ b/packages/camera/camera_windows/windows/record_handler.h @@ -16,6 +16,8 @@ namespace camera_windows { using Microsoft::WRL::ComPtr; enum class RecordingType { + // Camera is not recording. + kNone, // Recording continues until it is stopped with a separate stop command. kContinuous, // Recording stops automatically after requested record time is passed. @@ -43,7 +45,6 @@ class RecordHandler { // Initializes record sink and requests capture engine to start recording. // // Sets record state to: starting. - // Returns false if recording cannot be started. // // file_path: A string that hold file path for video capture. // max_duration: A int64 value of maximun recording duration. @@ -53,16 +54,15 @@ class RecordHandler { // the actual recording. // base_media_type: A pointer to base media type used as a base // for the actual video capture media type. - bool StartRecord(const std::string& file_path, int64_t max_duration, - IMFCaptureEngine* capture_engine, - IMFMediaType* base_media_type); + HRESULT StartRecord(const std::string& file_path, int64_t max_duration, + IMFCaptureEngine* capture_engine, + IMFMediaType* base_media_type); // Stops existing recording. - // Returns false if recording cannot be stopped. // // capture_engine: A pointer to capture engine instance. Used to stop // the ongoing recording. - bool StopRecord(IMFCaptureEngine* capture_engine); + HRESULT StopRecord(IMFCaptureEngine* capture_engine); // Set the record handler recording state to: running. void OnRecordStarted(); @@ -109,7 +109,7 @@ class RecordHandler { uint64_t recording_duration_us_ = 0; std::string file_path_; RecordState recording_state_ = RecordState::kNotStarted; - RecordingType type_; + RecordingType type_ = RecordingType::kNone; ComPtr record_sink_; }; diff --git a/packages/camera/camera_windows/windows/string_utils.cpp b/packages/camera/camera_windows/windows/string_utils.cpp index 2e60e1bb01a7..34b13361e71f 100644 --- a/packages/camera/camera_windows/windows/string_utils.cpp +++ b/packages/camera/camera_windows/windows/string_utils.cpp @@ -19,10 +19,10 @@ std::string Utf8FromUtf16(const std::wstring& utf16_string) { int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string.data(), static_cast(utf16_string.length()), nullptr, 0, nullptr, nullptr); - if (target_length == 0) { - return std::string(); - } std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string.data(), @@ -42,10 +42,10 @@ std::wstring Utf16FromUtf8(const std::string& utf8_string) { int target_length = ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8_string.data(), static_cast(utf8_string.length()), nullptr, 0); - if (target_length == 0) { - return std::wstring(); - } std::wstring utf16_string; + if (target_length == 0 || target_length > utf16_string.max_size()) { + return utf16_string; + } utf16_string.resize(target_length); int converted_length = ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8_string.data(), diff --git a/packages/camera/camera_windows/windows/test/camera_plugin_test.cpp b/packages/camera/camera_windows/windows/test/camera_plugin_test.cpp index 309268a1fb90..9cab069bbb97 100644 --- a/packages/camera/camera_windows/windows/test/camera_plugin_test.cpp +++ b/packages/camera/camera_windows/windows/test/camera_plugin_test.cpp @@ -30,6 +30,41 @@ using ::testing::Eq; using ::testing::Pointee; using ::testing::Return; +void MockInitCamera(MockCamera* camera, bool success) { + EXPECT_CALL(*camera, + HasPendingResultByType(Eq(PendingResultType::kCreateCamera))) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_CALL(*camera, + AddPendingResult(Eq(PendingResultType::kCreateCamera), _)) + .Times(1) + .WillOnce([camera](PendingResultType type, + std::unique_ptr> result) { + camera->pending_result_ = std::move(result); + return true; + }); + + EXPECT_CALL(*camera, HasDeviceId(Eq(camera->device_id_))) + .WillRepeatedly(Return(true)); + + EXPECT_CALL(*camera, InitCamera) + .Times(1) + .WillOnce([camera, success](flutter::TextureRegistrar* texture_registrar, + flutter::BinaryMessenger* messenger, + bool record_audio, + ResolutionPreset resolution_preset) { + assert(camera->pending_result_); + if (success) { + camera->pending_result_->Success(EncodableValue(1)); + return true; + } else { + camera->pending_result_->Error("camera_error", "InitCamera failed."); + return false; + } + }); +} + TEST(CameraPlugin, AvailableCamerasHandlerSuccessIfNoCameras) { std::unique_ptr texture_registrar_ = std::make_unique(); @@ -99,28 +134,7 @@ TEST(CameraPlugin, CreateHandlerCallsInitCamera) { std::unique_ptr camera = std::make_unique(MOCK_DEVICE_ID); - EXPECT_CALL(*camera, - HasPendingResultByType(Eq(PendingResultType::kCreateCamera))) - .Times(1) - .WillOnce(Return(false)); - - EXPECT_CALL(*camera, - AddPendingResult(Eq(PendingResultType::kCreateCamera), _)) - .Times(1) - .WillOnce([cam = camera.get()](PendingResultType type, - std::unique_ptr> result) { - cam->pending_result_ = std::move(result); - return true; - }); - EXPECT_CALL(*camera, InitCamera) - .Times(1) - .WillOnce([cam = camera.get()]( - flutter::TextureRegistrar* texture_registrar, - flutter::BinaryMessenger* messenger, bool record_audio, - ResolutionPreset resolution_preset) { - assert(cam->pending_result_); - return cam->pending_result_->Success(EncodableValue(1)); - }); + MockInitCamera(camera.get(), true); // Move mocked camera to the factory to be passed // for plugin with CreateCamera function. @@ -185,34 +199,7 @@ TEST(CameraPlugin, CreateHandlerErrorOnExistingDeviceId) { std::unique_ptr camera = std::make_unique(MOCK_DEVICE_ID); - EXPECT_CALL(*camera, - HasPendingResultByType(Eq(PendingResultType::kCreateCamera))) - .Times(1) - .WillOnce(Return(false)); - - EXPECT_CALL(*camera, - AddPendingResult(Eq(PendingResultType::kCreateCamera), _)) - .Times(1) - .WillOnce([cam = camera.get()](PendingResultType type, - std::unique_ptr> result) { - cam->pending_result_ = std::move(result); - return true; - }); - EXPECT_CALL(*camera, InitCamera) - .Times(1) - .WillOnce([cam = camera.get()]( - flutter::TextureRegistrar* texture_registrar, - flutter::BinaryMessenger* messenger, bool record_audio, - ResolutionPreset resolution_preset) { - assert(cam->pending_result_); - return cam->pending_result_->Success(EncodableValue(1)); - }); - - EXPECT_CALL(*camera, HasDeviceId(Eq(MOCK_DEVICE_ID))) - .Times(1) - .WillOnce([cam = camera.get()](std::string& device_id) { - return cam->device_id_ == device_id; - }); + MockInitCamera(camera.get(), true); // Move mocked camera to the factory to be passed // for plugin with CreateCamera function. @@ -246,6 +233,64 @@ TEST(CameraPlugin, CreateHandlerErrorOnExistingDeviceId) { std::move(second_create_result)); } +TEST(CameraPlugin, CreateHandlerAllowsRetry) { + std::unique_ptr first_create_result = + std::make_unique(); + std::unique_ptr second_create_result = + std::make_unique(); + std::unique_ptr texture_registrar_ = + std::make_unique(); + std::unique_ptr messenger_ = + std::make_unique(); + std::unique_ptr camera_factory_ = + std::make_unique(); + + // The camera will fail initialization once and then succeed. + EXPECT_CALL(*camera_factory_, CreateCamera(MOCK_DEVICE_ID)) + .Times(2) + .WillOnce([](const std::string& device_id) { + std::unique_ptr first_camera = + std::make_unique(MOCK_DEVICE_ID); + + MockInitCamera(first_camera.get(), false); + + return first_camera; + }) + .WillOnce([](const std::string& device_id) { + std::unique_ptr second_camera = + std::make_unique(MOCK_DEVICE_ID); + + MockInitCamera(second_camera.get(), true); + + return second_camera; + }); + + EXPECT_CALL(*first_create_result, ErrorInternal).Times(1); + EXPECT_CALL(*first_create_result, SuccessInternal).Times(0); + + CameraPlugin plugin(texture_registrar_.get(), messenger_.get(), + std::move(camera_factory_)); + EncodableMap args = { + {EncodableValue("cameraName"), EncodableValue(MOCK_CAMERA_NAME)}, + {EncodableValue("resolutionPreset"), EncodableValue(nullptr)}, + {EncodableValue("enableAudio"), EncodableValue(true)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("create", + std::make_unique(EncodableMap(args))), + std::move(first_create_result)); + + EXPECT_CALL(*second_create_result, ErrorInternal).Times(0); + EXPECT_CALL(*second_create_result, + SuccessInternal(Pointee(EncodableValue(1)))); + + plugin.HandleMethodCall( + flutter::MethodCall("create", + std::make_unique(EncodableMap(args))), + std::move(second_create_result)); +} + TEST(CameraPlugin, InitializeHandlerCallStartPreview) { int64_t mock_camera_id = 1234; diff --git a/packages/camera/camera_windows/windows/test/camera_test.cpp b/packages/camera/camera_windows/windows/test/camera_test.cpp index 899c1fdaea62..158a2c26c027 100644 --- a/packages/camera/camera_windows/windows/test/camera_test.cpp +++ b/packages/camera/camera_windows/windows/test/camera_test.cpp @@ -23,6 +23,7 @@ using ::testing::_; using ::testing::Eq; using ::testing::NiceMock; using ::testing::Pointee; +using ::testing::Return; namespace test { @@ -34,17 +35,57 @@ TEST(Camera, InitCameraCreatesCaptureController) { EXPECT_CALL(*capture_controller_factory, CreateCaptureController) .Times(1) - .WillOnce( - []() { return std::make_unique>(); }); + .WillOnce([]() { + std::unique_ptr> capture_controller = + std::make_unique>(); + + EXPECT_CALL(*capture_controller, InitCaptureDevice) + .Times(1) + .WillOnce(Return(true)); + + return capture_controller; + }); EXPECT_TRUE(camera->GetCaptureController() == nullptr); // Init camera with mock capture controller factory - camera->InitCamera(std::move(capture_controller_factory), - std::make_unique().get(), - std::make_unique().get(), false, - ResolutionPreset::kAuto); + bool result = + camera->InitCamera(std::move(capture_controller_factory), + std::make_unique().get(), + std::make_unique().get(), false, + ResolutionPreset::kAuto); + EXPECT_TRUE(result); + EXPECT_TRUE(camera->GetCaptureController() != nullptr); +} +TEST(Camera, InitCameraReportsFailure) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller_factory = + std::make_unique(); + + EXPECT_CALL(*capture_controller_factory, CreateCaptureController) + .Times(1) + .WillOnce([]() { + std::unique_ptr> capture_controller = + std::make_unique>(); + + EXPECT_CALL(*capture_controller, InitCaptureDevice) + .Times(1) + .WillOnce(Return(false)); + + return capture_controller; + }); + + EXPECT_TRUE(camera->GetCaptureController() == nullptr); + + // Init camera with mock capture controller factory + bool result = + camera->InitCamera(std::move(capture_controller_factory), + std::make_unique().get(), + std::make_unique().get(), false, + ResolutionPreset::kAuto); + EXPECT_FALSE(result); EXPECT_TRUE(camera->GetCaptureController() != nullptr); } @@ -90,20 +131,37 @@ TEST(Camera, OnCreateCaptureEngineSucceededReturnsCameraId) { camera->OnCreateCaptureEngineSucceeded(texture_id); } -TEST(Camera, OnCreateCaptureEngineFailedReturnsError) { +TEST(Camera, CreateCaptureEngineReportsError) { std::unique_ptr camera = std::make_unique(MOCK_DEVICE_ID); std::unique_ptr result = std::make_unique(); - std::string error_text = "error_text"; + const std::string error_text = "error_text"; EXPECT_CALL(*result, SuccessInternal).Times(0); - EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + EXPECT_CALL(*result, ErrorInternal(Eq("camera_error"), Eq(error_text), _)); camera->AddPendingResult(PendingResultType::kCreateCamera, std::move(result)); - camera->OnCreateCaptureEngineFailed(error_text); + camera->OnCreateCaptureEngineFailed(CameraResult::kError, error_text); +} + +TEST(Camera, CreateCaptureEngineReportsAccessDenied) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + const std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, + ErrorInternal(Eq("CameraAccessDenied"), Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kCreateCamera, std::move(result)); + + camera->OnCreateCaptureEngineFailed(CameraResult::kAccessDenied, error_text); } TEST(Camera, OnStartPreviewSucceededReturnsFrameSize) { @@ -128,20 +186,37 @@ TEST(Camera, OnStartPreviewSucceededReturnsFrameSize) { camera->OnStartPreviewSucceeded(width, height); } -TEST(Camera, OnStartPreviewFailedReturnsError) { +TEST(Camera, StartPreviewReportsError) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + const std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, ErrorInternal(Eq("camera_error"), Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kInitialize, std::move(result)); + + camera->OnStartPreviewFailed(CameraResult::kError, error_text); +} + +TEST(Camera, StartPreviewReportsAccessDenied) { std::unique_ptr camera = std::make_unique(MOCK_DEVICE_ID); std::unique_ptr result = std::make_unique(); - std::string error_text = "error_text"; + const std::string error_text = "error_text"; EXPECT_CALL(*result, SuccessInternal).Times(0); - EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + EXPECT_CALL(*result, + ErrorInternal(Eq("CameraAccessDenied"), Eq(error_text), _)); camera->AddPendingResult(PendingResultType::kInitialize, std::move(result)); - camera->OnStartPreviewFailed(error_text); + camera->OnStartPreviewFailed(CameraResult::kAccessDenied, error_text); } TEST(Camera, OnPausePreviewSucceededReturnsSuccess) { @@ -158,20 +233,37 @@ TEST(Camera, OnPausePreviewSucceededReturnsSuccess) { camera->OnPausePreviewSucceeded(); } -TEST(Camera, OnPausePreviewFailedReturnsError) { +TEST(Camera, PausePreviewReportsError) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + const std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, ErrorInternal(Eq("camera_error"), Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kPausePreview, std::move(result)); + + camera->OnPausePreviewFailed(CameraResult::kError, error_text); +} + +TEST(Camera, PausePreviewReportsAccessDenied) { std::unique_ptr camera = std::make_unique(MOCK_DEVICE_ID); std::unique_ptr result = std::make_unique(); - std::string error_text = "error_text"; + const std::string error_text = "error_text"; EXPECT_CALL(*result, SuccessInternal).Times(0); - EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + EXPECT_CALL(*result, + ErrorInternal(Eq("CameraAccessDenied"), Eq(error_text), _)); camera->AddPendingResult(PendingResultType::kPausePreview, std::move(result)); - camera->OnPausePreviewFailed(error_text); + camera->OnPausePreviewFailed(CameraResult::kAccessDenied, error_text); } TEST(Camera, OnResumePreviewSucceededReturnsSuccess) { @@ -189,21 +281,39 @@ TEST(Camera, OnResumePreviewSucceededReturnsSuccess) { camera->OnResumePreviewSucceeded(); } -TEST(Camera, OnResumePreviewFailedReturnsError) { +TEST(Camera, ResumePreviewReportsError) { std::unique_ptr camera = std::make_unique(MOCK_DEVICE_ID); std::unique_ptr result = std::make_unique(); - std::string error_text = "error_text"; + const std::string error_text = "error_text"; EXPECT_CALL(*result, SuccessInternal).Times(0); - EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + EXPECT_CALL(*result, ErrorInternal(Eq("camera_error"), Eq(error_text), _)); camera->AddPendingResult(PendingResultType::kResumePreview, std::move(result)); - camera->OnResumePreviewFailed(error_text); + camera->OnResumePreviewFailed(CameraResult::kError, error_text); +} + +TEST(Camera, OnResumePreviewPermissionFailureReturnsError) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + const std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, + ErrorInternal(Eq("CameraAccessDenied"), Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kResumePreview, + std::move(result)); + + camera->OnResumePreviewFailed(CameraResult::kAccessDenied, error_text); } TEST(Camera, OnStartRecordSucceededReturnsSuccess) { @@ -220,20 +330,37 @@ TEST(Camera, OnStartRecordSucceededReturnsSuccess) { camera->OnStartRecordSucceeded(); } -TEST(Camera, OnStartRecordFailedReturnsError) { +TEST(Camera, StartRecordReportsError) { std::unique_ptr camera = std::make_unique(MOCK_DEVICE_ID); std::unique_ptr result = std::make_unique(); - std::string error_text = "error_text"; + const std::string error_text = "error_text"; EXPECT_CALL(*result, SuccessInternal).Times(0); - EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + EXPECT_CALL(*result, ErrorInternal(Eq("camera_error"), Eq(error_text), _)); camera->AddPendingResult(PendingResultType::kStartRecord, std::move(result)); - camera->OnStartRecordFailed(error_text); + camera->OnStartRecordFailed(CameraResult::kError, error_text); +} + +TEST(Camera, StartRecordReportsAccessDenied) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + const std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, + ErrorInternal(Eq("CameraAccessDenied"), Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kStartRecord, std::move(result)); + + camera->OnStartRecordFailed(CameraResult::kAccessDenied, error_text); } TEST(Camera, OnStopRecordSucceededReturnsSuccess) { @@ -242,7 +369,7 @@ TEST(Camera, OnStopRecordSucceededReturnsSuccess) { std::unique_ptr result = std::make_unique(); - std::string file_path = "C:\temp\filename.mp4"; + const std::string file_path = "C:\temp\filename.mp4"; EXPECT_CALL(*result, ErrorInternal).Times(0); EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(file_path)))); @@ -252,20 +379,37 @@ TEST(Camera, OnStopRecordSucceededReturnsSuccess) { camera->OnStopRecordSucceeded(file_path); } -TEST(Camera, OnStopRecordFailedReturnsError) { +TEST(Camera, StopRecordReportsError) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + const std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, ErrorInternal(Eq("camera_error"), Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kStopRecord, std::move(result)); + + camera->OnStopRecordFailed(CameraResult::kError, error_text); +} + +TEST(Camera, StopRecordReportsAccessDenied) { std::unique_ptr camera = std::make_unique(MOCK_DEVICE_ID); std::unique_ptr result = std::make_unique(); - std::string error_text = "error_text"; + const std::string error_text = "error_text"; EXPECT_CALL(*result, SuccessInternal).Times(0); - EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + EXPECT_CALL(*result, + ErrorInternal(Eq("CameraAccessDenied"), Eq(error_text), _)); camera->AddPendingResult(PendingResultType::kStopRecord, std::move(result)); - camera->OnStopRecordFailed(error_text); + camera->OnStopRecordFailed(CameraResult::kAccessDenied, error_text); } TEST(Camera, OnTakePictureSucceededReturnsSuccess) { @@ -274,7 +418,7 @@ TEST(Camera, OnTakePictureSucceededReturnsSuccess) { std::unique_ptr result = std::make_unique(); - std::string file_path = "C:\temp\filename.jpeg"; + const std::string file_path = "C:\\temp\\filename.jpeg"; EXPECT_CALL(*result, ErrorInternal).Times(0); EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(file_path)))); @@ -284,20 +428,37 @@ TEST(Camera, OnTakePictureSucceededReturnsSuccess) { camera->OnTakePictureSucceeded(file_path); } -TEST(Camera, OnTakePictureFailedReturnsError) { +TEST(Camera, TakePictureReportsError) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + const std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, ErrorInternal(Eq("camera_error"), Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kTakePicture, std::move(result)); + + camera->OnTakePictureFailed(CameraResult::kError, error_text); +} + +TEST(Camera, TakePictureReportsAccessDenied) { std::unique_ptr camera = std::make_unique(MOCK_DEVICE_ID); std::unique_ptr result = std::make_unique(); - std::string error_text = "error_text"; + const std::string error_text = "error_text"; EXPECT_CALL(*result, SuccessInternal).Times(0); - EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + EXPECT_CALL(*result, + ErrorInternal(Eq("CameraAccessDenied"), Eq(error_text), _)); camera->AddPendingResult(PendingResultType::kTakePicture, std::move(result)); - camera->OnTakePictureFailed(error_text); + camera->OnTakePictureFailed(CameraResult::kAccessDenied, error_text); } TEST(Camera, OnVideoRecordSucceededInvokesCameraChannelEvent) { @@ -309,12 +470,12 @@ TEST(Camera, OnVideoRecordSucceededInvokesCameraChannelEvent) { std::unique_ptr binary_messenger = std::make_unique(); - std::string file_path = "C:\temp\filename.mp4"; - int64_t camera_id = 12345; + const std::string file_path = "C:\\temp\\filename.mp4"; + const int64_t camera_id = 12345; std::string camera_channel = std::string("plugins.flutter.io/camera_windows/camera") + std::to_string(camera_id); - int64_t video_duration = 1000000; + const int64_t video_duration = 1000000; EXPECT_CALL(*capture_controller_factory, CreateCaptureController) .Times(1) diff --git a/packages/camera/camera_windows/windows/test/capture_controller_test.cpp b/packages/camera/camera_windows/windows/test/capture_controller_test.cpp index 7520af7a4af8..8d6632cbc3f0 100644 --- a/packages/camera/camera_windows/windows/test/capture_controller_test.cpp +++ b/packages/camera/camera_windows/windows/test/capture_controller_test.cpp @@ -59,8 +59,10 @@ void MockInitCaptureController(CaptureControllerImpl* capture_controller, .Times(1); EXPECT_CALL(*engine, Initialize).Times(1); - capture_controller->InitCaptureDevice(texture_registrar, MOCK_DEVICE_ID, true, - ResolutionPreset::kAuto); + bool result = capture_controller->InitCaptureDevice( + texture_registrar, MOCK_DEVICE_ID, true, ResolutionPreset::kAuto); + + EXPECT_TRUE(result); // MockCaptureEngine::Initialize is called EXPECT_TRUE(engine->initialized_); @@ -68,35 +70,10 @@ void MockInitCaptureController(CaptureControllerImpl* capture_controller, engine->CreateFakeEvent(S_OK, MF_CAPTURE_ENGINE_INITIALIZED); } -void MockStartPreview(CaptureControllerImpl* capture_controller, - MockCaptureSource* capture_source, - MockCapturePreviewSink* preview_sink, - MockTextureRegistrar* texture_registrar, - MockCaptureEngine* engine, MockCamera* camera, - std::unique_ptr mock_source_buffer, - uint32_t mock_source_buffer_size, - uint32_t mock_preview_width, uint32_t mock_preview_height, - int64_t mock_texture_id) { - EXPECT_CALL(*engine, GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_PREVIEW, _)) - .Times(1) - .WillOnce([src_sink = preview_sink](MF_CAPTURE_ENGINE_SINK_TYPE sink_type, - IMFCaptureSink** target_sink) { - *target_sink = src_sink; - src_sink->AddRef(); - return S_OK; - }); - - EXPECT_CALL(*preview_sink, RemoveAllStreams).Times(1).WillOnce(Return(S_OK)); - EXPECT_CALL(*preview_sink, AddStream).Times(1).WillOnce(Return(S_OK)); - EXPECT_CALL(*preview_sink, SetSampleCallback) - .Times(1) - .WillOnce([sink = preview_sink]( - DWORD dwStreamSinkIndex, - IMFCaptureEngineOnSampleCallback* pCallback) -> HRESULT { - sink->sample_callback_ = pCallback; - return S_OK; - }); - +void MockAvailableMediaTypes(MockCaptureEngine* engine, + MockCaptureSource* capture_source, + uint32_t mock_preview_width, + uint32_t mock_preview_height) { EXPECT_CALL(*engine, GetSource) .Times(1) .WillOnce( @@ -140,6 +117,39 @@ void MockStartPreview(CaptureControllerImpl* capture_controller, (*media_type)->AddRef(); return S_OK; }); +} + +void MockStartPreview(CaptureControllerImpl* capture_controller, + MockCapturePreviewSink* preview_sink, + MockTextureRegistrar* texture_registrar, + MockCaptureEngine* engine, MockCamera* camera, + std::unique_ptr mock_source_buffer, + uint32_t mock_source_buffer_size, + uint32_t mock_preview_width, uint32_t mock_preview_height, + int64_t mock_texture_id) { + EXPECT_CALL(*engine, GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_PREVIEW, _)) + .Times(1) + .WillOnce([src_sink = preview_sink](MF_CAPTURE_ENGINE_SINK_TYPE sink_type, + IMFCaptureSink** target_sink) { + *target_sink = src_sink; + src_sink->AddRef(); + return S_OK; + }); + + EXPECT_CALL(*preview_sink, RemoveAllStreams).Times(1).WillOnce(Return(S_OK)); + EXPECT_CALL(*preview_sink, AddStream).Times(1).WillOnce(Return(S_OK)); + EXPECT_CALL(*preview_sink, SetSampleCallback) + .Times(1) + .WillOnce([sink = preview_sink]( + DWORD dwStreamSinkIndex, + IMFCaptureEngineOnSampleCallback* pCallback) -> HRESULT { + sink->sample_callback_ = pCallback; + return S_OK; + }); + + ComPtr capture_source = new MockCaptureSource(); + MockAvailableMediaTypes(engine, capture_source.Get(), mock_preview_width, + mock_preview_height); EXPECT_CALL(*engine, StartPreview()).Times(1).WillOnce(Return(S_OK)); @@ -167,6 +177,21 @@ void MockStartPreview(CaptureControllerImpl* capture_controller, mock_source_buffer_size); } +void MockPhotoSink(MockCaptureEngine* engine, + MockCapturePhotoSink* photo_sink) { + EXPECT_CALL(*engine, GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_PHOTO, _)) + .Times(1) + .WillOnce([src_sink = photo_sink](MF_CAPTURE_ENGINE_SINK_TYPE sink_type, + IMFCaptureSink** target_sink) { + *target_sink = src_sink; + src_sink->AddRef(); + return S_OK; + }); + EXPECT_CALL(*photo_sink, RemoveAllStreams).Times(1).WillOnce(Return(S_OK)); + EXPECT_CALL(*photo_sink, AddStream).Times(1).WillOnce(Return(S_OK)); + EXPECT_CALL(*photo_sink, SetOutputFileName).Times(1).WillOnce(Return(S_OK)); +} + void MockRecordStart(CaptureControllerImpl* capture_controller, MockCaptureEngine* engine, MockCaptureRecordSink* record_sink, MockCamera* camera, @@ -202,7 +227,7 @@ TEST(CaptureController, std::unique_ptr texture_registrar = std::make_unique(); - uint64_t mock_texture_id = 1234; + int64_t mock_texture_id = 1234; // Init capture controller with mocks and tests MockInitCaptureController(capture_controller.get(), texture_registrar.get(), @@ -214,6 +239,233 @@ TEST(CaptureController, engine = nullptr; } +TEST(CaptureController, InitCaptureEngineCanOnlyBeCalledOnce) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Init capture controller once with mocks and tests + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + // Init capture controller a second time. + EXPECT_CALL(*camera, OnCreateCaptureEngineFailed).Times(1); + + bool result = capture_controller->InitCaptureDevice( + texture_registrar.get(), MOCK_DEVICE_ID, true, ResolutionPreset::kAuto); + + EXPECT_FALSE(result); + + capture_controller = nullptr; + camera = nullptr; + texture_registrar = nullptr; + engine = nullptr; +} + +TEST(CaptureController, InitCaptureEngineReportsFailure) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + ComPtr video_source = new MockMediaSource(); + ComPtr audio_source = new MockMediaSource(); + + capture_controller->SetCaptureEngine( + reinterpret_cast(engine.Get())); + capture_controller->SetVideoSource( + reinterpret_cast(video_source.Get())); + capture_controller->SetAudioSource( + reinterpret_cast(audio_source.Get())); + + // Cause initialization to fail + EXPECT_CALL(*engine.Get(), Initialize).Times(1).WillOnce(Return(E_FAIL)); + + EXPECT_CALL(*texture_registrar, RegisterTexture).Times(0); + EXPECT_CALL(*texture_registrar, UnregisterTexture(_)).Times(0); + EXPECT_CALL(*camera, OnCreateCaptureEngineSucceeded).Times(0); + EXPECT_CALL(*camera, + OnCreateCaptureEngineFailed(Eq(CameraResult::kError), + Eq("Failed to create camera"))) + .Times(1); + + bool result = capture_controller->InitCaptureDevice( + texture_registrar.get(), MOCK_DEVICE_ID, true, ResolutionPreset::kAuto); + + EXPECT_FALSE(result); + EXPECT_FALSE(engine->initialized_); + + capture_controller = nullptr; + camera = nullptr; + texture_registrar = nullptr; + engine = nullptr; +} + +TEST(CaptureController, InitCaptureEngineReportsAccessDenied) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + ComPtr video_source = new MockMediaSource(); + ComPtr audio_source = new MockMediaSource(); + + capture_controller->SetCaptureEngine( + reinterpret_cast(engine.Get())); + capture_controller->SetVideoSource( + reinterpret_cast(video_source.Get())); + capture_controller->SetAudioSource( + reinterpret_cast(audio_source.Get())); + + // Cause initialization to fail + EXPECT_CALL(*engine.Get(), Initialize) + .Times(1) + .WillOnce(Return(E_ACCESSDENIED)); + + EXPECT_CALL(*texture_registrar, RegisterTexture).Times(0); + EXPECT_CALL(*texture_registrar, UnregisterTexture(_)).Times(0); + EXPECT_CALL(*camera, OnCreateCaptureEngineSucceeded).Times(0); + EXPECT_CALL(*camera, + OnCreateCaptureEngineFailed(Eq(CameraResult::kAccessDenied), + Eq("Failed to create camera"))) + .Times(1); + + bool result = capture_controller->InitCaptureDevice( + texture_registrar.get(), MOCK_DEVICE_ID, true, ResolutionPreset::kAuto); + + EXPECT_FALSE(result); + EXPECT_FALSE(engine->initialized_); + + capture_controller = nullptr; + camera = nullptr; + texture_registrar = nullptr; + engine = nullptr; +} + +TEST(CaptureController, ReportsInitializedErrorEvent) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + EXPECT_CALL(*camera, OnCreateCaptureEngineFailed( + Eq(CameraResult::kError), + Eq("Failed to initialize capture engine"))) + .Times(1); + EXPECT_CALL(*camera, OnCreateCaptureEngineSucceeded).Times(0); + + // Send initialization failed event + engine->CreateFakeEvent(E_FAIL, MF_CAPTURE_ENGINE_INITIALIZED); + + capture_controller = nullptr; + camera = nullptr; + texture_registrar = nullptr; + engine = nullptr; +} + +TEST(CaptureController, ReportsInitializedAccessDeniedEvent) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + EXPECT_CALL(*camera, OnCreateCaptureEngineFailed( + Eq(CameraResult::kAccessDenied), + Eq("Failed to initialize capture engine"))) + .Times(1); + EXPECT_CALL(*camera, OnCreateCaptureEngineSucceeded).Times(0); + + // Send initialization failed event + engine->CreateFakeEvent(E_ACCESSDENIED, MF_CAPTURE_ENGINE_INITIALIZED); + + capture_controller = nullptr; + camera = nullptr; + texture_registrar = nullptr; + engine = nullptr; +} + +TEST(CaptureController, ReportsCaptureEngineErrorEvent) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + EXPECT_CALL(*(camera.get()), + OnCaptureError(Eq(CameraResult::kError), Eq("Unspecified error"))) + .Times(1); + + // Send error event. + engine->CreateFakeEvent(E_FAIL, MF_CAPTURE_ENGINE_ERROR); + + capture_controller = nullptr; + camera = nullptr; + texture_registrar = nullptr; + engine = nullptr; +} + +TEST(CaptureController, ReportsCaptureEngineAccessDeniedEvent) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + EXPECT_CALL(*(camera.get()), OnCaptureError(Eq(CameraResult::kAccessDenied), + Eq("Access is denied."))) + .Times(1); + + // Send error event. + engine->CreateFakeEvent(E_ACCESSDENIED, MF_CAPTURE_ENGINE_ERROR); + + capture_controller = nullptr; + camera = nullptr; + texture_registrar = nullptr; + engine = nullptr; +} + TEST(CaptureController, StartPreviewStartsProcessingSamples) { ComPtr engine = new MockCaptureEngine(); std::unique_ptr camera = @@ -223,14 +475,13 @@ TEST(CaptureController, StartPreviewStartsProcessingSamples) { std::unique_ptr texture_registrar = std::make_unique(); - uint64_t mock_texture_id = 1234; + int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), engine.Get(), camera.get(), mock_texture_id); ComPtr preview_sink = new MockCapturePreviewSink(); - ComPtr capture_source = new MockCaptureSource(); // Let's keep these small for mock texture data. Two pixels should be // enough. @@ -258,11 +509,10 @@ TEST(CaptureController, StartPreviewStartsProcessingSamples) { } // Start preview and run preview tests - MockStartPreview(capture_controller.get(), capture_source.Get(), - preview_sink.Get(), texture_registrar.get(), engine.Get(), - camera.get(), std::move(mock_source_buffer), - mock_texture_data_size, mock_preview_width, - mock_preview_height, mock_texture_id); + MockStartPreview(capture_controller.get(), preview_sink.Get(), + texture_registrar.get(), engine.Get(), camera.get(), + std::move(mock_source_buffer), mock_texture_data_size, + mock_preview_width, mock_preview_height, mock_texture_id); // Test texture processing EXPECT_TRUE(texture_registrar->texture_); @@ -303,7 +553,7 @@ TEST(CaptureController, StartPreviewStartsProcessingSamples) { texture_registrar = nullptr; } -TEST(CaptureController, StartRecordSuccess) { +TEST(CaptureController, ReportsStartPreviewError) { ComPtr engine = new MockCaptureEngine(); std::unique_ptr camera = std::make_unique(MOCK_DEVICE_ID); @@ -312,43 +562,488 @@ TEST(CaptureController, StartRecordSuccess) { std::unique_ptr texture_registrar = std::make_unique(); - uint64_t mock_texture_id = 1234; + int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), engine.Get(), camera.get(), mock_texture_id); - ComPtr preview_sink = new MockCapturePreviewSink(); ComPtr capture_source = new MockCaptureSource(); + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); - std::unique_ptr mock_source_buffer = - std::make_unique(0); - - // Start preview to be able to start record - MockStartPreview(capture_controller.get(), capture_source.Get(), - preview_sink.Get(), texture_registrar.get(), engine.Get(), - camera.get(), std::move(mock_source_buffer), 0, 1, 1, - mock_texture_id); + // Cause start preview to fail + EXPECT_CALL(*engine.Get(), GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_PREVIEW, _)) + .Times(1) + .WillOnce(Return(E_FAIL)); - // Start record - ComPtr record_sink = new MockCaptureRecordSink(); - std::string mock_path_to_video = "mock_path_to_video"; - MockRecordStart(capture_controller.get(), engine.Get(), record_sink.Get(), - camera.get(), mock_path_to_video); + EXPECT_CALL(*engine.Get(), StartPreview).Times(0); + EXPECT_CALL(*engine.Get(), StopPreview).Times(0); + EXPECT_CALL(*camera, OnStartPreviewSucceeded).Times(0); + EXPECT_CALL(*camera, + OnStartPreviewFailed(Eq(CameraResult::kError), + Eq("Failed to start video preview"))) + .Times(1); - // Called by destructor - EXPECT_CALL(*(engine.Get()), StopRecord(true, false)) - .Times(1) - .WillOnce(Return(S_OK)); + capture_controller->StartPreview(); capture_controller = nullptr; - texture_registrar = nullptr; engine = nullptr; camera = nullptr; - record_sink = nullptr; + texture_registrar = nullptr; } -TEST(CaptureController, StopRecordSuccess) { +// TODO(loic-sharma): Test duplicate calls to start preview. + +TEST(CaptureController, IgnoresStartPreviewErrorEvent) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + EXPECT_CALL(*camera, OnStartPreviewFailed).Times(0); + EXPECT_CALL(*camera, OnCreateCaptureEngineSucceeded).Times(0); + + // Send a start preview error event + engine->CreateFakeEvent(E_FAIL, MF_CAPTURE_ENGINE_PREVIEW_STARTED); + + capture_controller = nullptr; + camera = nullptr; + texture_registrar = nullptr; + engine = nullptr; +} + +TEST(CaptureController, ReportsStartPreviewAccessDenied) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); + + // Cause start preview to fail + EXPECT_CALL(*engine.Get(), GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_PREVIEW, _)) + .Times(1) + .WillOnce(Return(E_ACCESSDENIED)); + + EXPECT_CALL(*engine.Get(), StartPreview).Times(0); + EXPECT_CALL(*engine.Get(), StopPreview).Times(0); + EXPECT_CALL(*camera, OnStartPreviewSucceeded).Times(0); + EXPECT_CALL(*camera, + OnStartPreviewFailed(Eq(CameraResult::kAccessDenied), + Eq("Failed to start video preview"))) + .Times(1); + + capture_controller->StartPreview(); + + capture_controller = nullptr; + engine = nullptr; + camera = nullptr; + texture_registrar = nullptr; +} + +TEST(CaptureController, StartRecordSuccess) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); + + // Start record + ComPtr record_sink = new MockCaptureRecordSink(); + std::string mock_path_to_video = "mock_path_to_video"; + MockRecordStart(capture_controller.get(), engine.Get(), record_sink.Get(), + camera.get(), mock_path_to_video); + + // Called by destructor + EXPECT_CALL(*(engine.Get()), StopRecord(true, false)) + .Times(1) + .WillOnce(Return(S_OK)); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; + record_sink = nullptr; +} + +TEST(CaptureController, ReportsStartRecordError) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); + + // Cause start record to fail + EXPECT_CALL(*engine.Get(), GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_RECORD, _)) + .Times(1) + .WillOnce(Return(E_FAIL)); + + EXPECT_CALL(*engine.Get(), StartRecord).Times(0); + EXPECT_CALL(*engine.Get(), StopRecord).Times(0); + EXPECT_CALL(*camera, OnStartRecordSucceeded).Times(0); + EXPECT_CALL(*camera, + OnStartRecordFailed(Eq(CameraResult::kError), + Eq("Failed to start video recording"))) + .Times(1); + + capture_controller->StartRecord("mock_path", -1); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; +} + +TEST(CaptureController, ReportsStartRecordAccessDenied) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); + + // Cause start record to fail + EXPECT_CALL(*engine.Get(), GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_RECORD, _)) + .Times(1) + .WillOnce(Return(E_ACCESSDENIED)); + + EXPECT_CALL(*engine.Get(), StartRecord).Times(0); + EXPECT_CALL(*engine.Get(), StopRecord).Times(0); + EXPECT_CALL(*camera, OnStartRecordSucceeded).Times(0); + EXPECT_CALL(*camera, + OnStartRecordFailed(Eq(CameraResult::kAccessDenied), + Eq("Failed to start video recording"))) + .Times(1); + + capture_controller->StartRecord("mock_path", -1); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; +} + +TEST(CaptureController, ReportsStartRecordErrorEvent) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); + + // Start record + ComPtr record_sink = new MockCaptureRecordSink(); + std::string mock_path_to_video = "mock_path_to_video"; + + EXPECT_CALL(*engine.Get(), StartRecord()).Times(1).WillOnce(Return(S_OK)); + + EXPECT_CALL(*engine.Get(), GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_RECORD, _)) + .Times(1) + .WillOnce([src_sink = record_sink](MF_CAPTURE_ENGINE_SINK_TYPE sink_type, + IMFCaptureSink** target_sink) { + *target_sink = src_sink.Get(); + src_sink->AddRef(); + return S_OK; + }); + + EXPECT_CALL(*record_sink.Get(), RemoveAllStreams) + .Times(1) + .WillOnce(Return(S_OK)); + EXPECT_CALL(*record_sink.Get(), AddStream) + .Times(2) + .WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*record_sink.Get(), SetOutputFileName) + .Times(1) + .WillOnce(Return(S_OK)); + + capture_controller->StartRecord(mock_path_to_video, -1); + + // Send a start record failed event + EXPECT_CALL(*camera, OnStartRecordSucceeded).Times(0); + EXPECT_CALL(*camera, OnStartRecordFailed(Eq(CameraResult::kError), + Eq("Unspecified error"))) + .Times(1); + + engine->CreateFakeEvent(E_FAIL, MF_CAPTURE_ENGINE_RECORD_STARTED); + + // Destructor shouldn't attempt to stop the recording that failed to start. + EXPECT_CALL(*engine.Get(), StopRecord).Times(0); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; + record_sink = nullptr; +} + +TEST(CaptureController, ReportsStartRecordAccessDeniedEvent) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); + + // Start record + ComPtr record_sink = new MockCaptureRecordSink(); + std::string mock_path_to_video = "mock_path_to_video"; + + EXPECT_CALL(*engine.Get(), StartRecord()).Times(1).WillOnce(Return(S_OK)); + + EXPECT_CALL(*engine.Get(), GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_RECORD, _)) + .Times(1) + .WillOnce([src_sink = record_sink](MF_CAPTURE_ENGINE_SINK_TYPE sink_type, + IMFCaptureSink** target_sink) { + *target_sink = src_sink.Get(); + src_sink->AddRef(); + return S_OK; + }); + + EXPECT_CALL(*record_sink.Get(), RemoveAllStreams) + .Times(1) + .WillOnce(Return(S_OK)); + EXPECT_CALL(*record_sink.Get(), AddStream) + .Times(2) + .WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*record_sink.Get(), SetOutputFileName) + .Times(1) + .WillOnce(Return(S_OK)); + + // Send a start record failed event + capture_controller->StartRecord(mock_path_to_video, -1); + + EXPECT_CALL(*camera, OnStartRecordSucceeded).Times(0); + EXPECT_CALL(*camera, OnStartRecordFailed(Eq(CameraResult::kAccessDenied), + Eq("Access is denied."))) + .Times(1); + + engine->CreateFakeEvent(E_ACCESSDENIED, MF_CAPTURE_ENGINE_RECORD_STARTED); + + // Destructor shouldn't attempt to stop the recording that failed to start. + EXPECT_CALL(*engine.Get(), StopRecord).Times(0); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; + record_sink = nullptr; +} + +TEST(CaptureController, StopRecordSuccess) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); + + // Start record + ComPtr record_sink = new MockCaptureRecordSink(); + std::string mock_path_to_video = "mock_path_to_video"; + MockRecordStart(capture_controller.get(), engine.Get(), record_sink.Get(), + camera.get(), mock_path_to_video); + + // Request to stop record + EXPECT_CALL(*(engine.Get()), StopRecord(true, false)) + .Times(1) + .WillOnce(Return(S_OK)); + capture_controller->StopRecord(); + + // OnStopRecordSucceeded should be called with mocked file path + EXPECT_CALL(*camera, OnStopRecordSucceeded(Eq(mock_path_to_video))).Times(1); + EXPECT_CALL(*camera, OnStopRecordFailed).Times(0); + + engine->CreateFakeEvent(S_OK, MF_CAPTURE_ENGINE_RECORD_STOPPED); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; + record_sink = nullptr; +} + +TEST(CaptureController, ReportsStopRecordError) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); + + // Start record + ComPtr record_sink = new MockCaptureRecordSink(); + MockRecordStart(capture_controller.get(), engine.Get(), record_sink.Get(), + camera.get(), "mock_path_to_video"); + + // Cause stop record to fail + EXPECT_CALL(*(engine.Get()), StopRecord(true, false)) + .Times(1) + .WillOnce(Return(E_FAIL)); + + EXPECT_CALL(*camera, OnStopRecordSucceeded).Times(0); + EXPECT_CALL(*camera, OnStopRecordFailed(Eq(CameraResult::kError), + Eq("Failed to stop video recording"))) + .Times(1); + + capture_controller->StopRecord(); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; + record_sink = nullptr; +} + +TEST(CaptureController, ReportsStopRecordAccessDenied) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); + + // Start record + ComPtr record_sink = new MockCaptureRecordSink(); + MockRecordStart(capture_controller.get(), engine.Get(), record_sink.Get(), + camera.get(), "mock_path_to_video"); + + // Cause stop record to fail + EXPECT_CALL(*(engine.Get()), StopRecord(true, false)) + .Times(1) + .WillOnce(Return(E_ACCESSDENIED)); + + EXPECT_CALL(*camera, OnStopRecordSucceeded).Times(0); + EXPECT_CALL(*camera, OnStopRecordFailed(Eq(CameraResult::kAccessDenied), + Eq("Failed to stop video recording"))) + .Times(1); + + capture_controller->StopRecord(); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; + record_sink = nullptr; +} + +TEST(CaptureController, ReportsStopRecordErrorEvent) { ComPtr engine = new MockCaptureEngine(); std::unique_ptr camera = std::make_unique(MOCK_DEVICE_ID); @@ -357,23 +1052,16 @@ TEST(CaptureController, StopRecordSuccess) { std::unique_ptr texture_registrar = std::make_unique(); - uint64_t mock_texture_id = 1234; + int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), engine.Get(), camera.get(), mock_texture_id); - ComPtr preview_sink = new MockCapturePreviewSink(); ComPtr capture_source = new MockCaptureSource(); - std::unique_ptr mock_source_buffer = - std::make_unique(0); - - // Start preview to be able to start record - MockStartPreview(capture_controller.get(), capture_source.Get(), - preview_sink.Get(), texture_registrar.get(), engine.Get(), - camera.get(), std::move(mock_source_buffer), 0, 1, 1, - mock_texture_id); + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); // Start record ComPtr record_sink = new MockCaptureRecordSink(); @@ -381,15 +1069,13 @@ TEST(CaptureController, StopRecordSuccess) { MockRecordStart(capture_controller.get(), engine.Get(), record_sink.Get(), camera.get(), mock_path_to_video); - // Request to stop record - EXPECT_CALL(*(engine.Get()), StopRecord(true, false)) - .Times(1) - .WillOnce(Return(S_OK)); - capture_controller->StopRecord(); + // Send a stop record failure event + EXPECT_CALL(*camera, OnStopRecordSucceeded).Times(0); + EXPECT_CALL(*camera, OnStopRecordFailed(Eq(CameraResult::kError), + Eq("Unspecified error"))) + .Times(1); - // OnStopRecordSucceeded should be called with mocked file path - EXPECT_CALL(*camera, OnStopRecordSucceeded(Eq(mock_path_to_video))).Times(1); - engine->CreateFakeEvent(S_OK, MF_CAPTURE_ENGINE_RECORD_STOPPED); + engine->CreateFakeEvent(E_FAIL, MF_CAPTURE_ENGINE_RECORD_STOPPED); capture_controller = nullptr; texture_registrar = nullptr; @@ -398,7 +1084,7 @@ TEST(CaptureController, StopRecordSuccess) { record_sink = nullptr; } -TEST(CaptureController, TakePictureSuccess) { +TEST(CaptureController, ReportsStopRecordAccessDeniedEvent) { ComPtr engine = new MockCaptureEngine(); std::unique_ptr camera = std::make_unique(MOCK_DEVICE_ID); @@ -407,42 +1093,62 @@ TEST(CaptureController, TakePictureSuccess) { std::unique_ptr texture_registrar = std::make_unique(); - uint64_t mock_texture_id = 1234; + int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), engine.Get(), camera.get(), mock_texture_id); - ComPtr preview_sink = new MockCapturePreviewSink(); ComPtr capture_source = new MockCaptureSource(); - std::unique_ptr mock_source_buffer = - std::make_unique(0); + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); - // Start preview to be able to start record - MockStartPreview(capture_controller.get(), capture_source.Get(), - preview_sink.Get(), texture_registrar.get(), engine.Get(), - camera.get(), std::move(mock_source_buffer), 0, 1, 1, - mock_texture_id); + // Start record + ComPtr record_sink = new MockCaptureRecordSink(); + std::string mock_path_to_video = "mock_path_to_video"; + MockRecordStart(capture_controller.get(), engine.Get(), record_sink.Get(), + camera.get(), mock_path_to_video); + + // Send a stop record failure event + EXPECT_CALL(*camera, OnStopRecordSucceeded).Times(0); + EXPECT_CALL(*camera, OnStopRecordFailed(Eq(CameraResult::kAccessDenied), + Eq("Access is denied."))) + .Times(1); + + engine->CreateFakeEvent(E_ACCESSDENIED, MF_CAPTURE_ENGINE_RECORD_STOPPED); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; + record_sink = nullptr; +} + +TEST(CaptureController, TakePictureSuccess) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to take picture + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); - // Init photo sink tests ComPtr photo_sink = new MockCapturePhotoSink(); - EXPECT_CALL(*(engine.Get()), GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_PHOTO, _)) - .Times(1) - .WillOnce( - [src_sink = photo_sink.Get()](MF_CAPTURE_ENGINE_SINK_TYPE sink_type, - IMFCaptureSink** target_sink) { - *target_sink = src_sink; - src_sink->AddRef(); - return S_OK; - }); - EXPECT_CALL(*(photo_sink.Get()), RemoveAllStreams) - .Times(1) - .WillOnce(Return(S_OK)); - EXPECT_CALL(*(photo_sink.Get()), AddStream).Times(1).WillOnce(Return(S_OK)); - EXPECT_CALL(*(photo_sink.Get()), SetOutputFileName) - .Times(1) - .WillOnce(Return(S_OK)); + + // Initialize photo sink + MockPhotoSink(engine.Get(), photo_sink.Get()); // Request photo std::string mock_path_to_photo = "mock_path_to_photo"; @@ -451,6 +1157,7 @@ TEST(CaptureController, TakePictureSuccess) { // OnTakePictureSucceeded should be called with mocked file path EXPECT_CALL(*camera, OnTakePictureSucceeded(Eq(mock_path_to_photo))).Times(1); + EXPECT_CALL(*camera, OnTakePictureFailed).Times(0); engine->CreateFakeEvent(S_OK, MF_CAPTURE_ENGINE_PHOTO_TAKEN); capture_controller = nullptr; @@ -460,6 +1167,182 @@ TEST(CaptureController, TakePictureSuccess) { photo_sink = nullptr; } +TEST(CaptureController, ReportsTakePictureError) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to take picture + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); + + ComPtr photo_sink = new MockCapturePhotoSink(); + + // Initialize photo sink + MockPhotoSink(engine.Get(), photo_sink.Get()); + + // Cause take picture to fail + EXPECT_CALL(*(engine.Get()), TakePhoto).Times(1).WillOnce(Return(E_FAIL)); + + EXPECT_CALL(*camera, OnTakePictureSucceeded).Times(0); + EXPECT_CALL(*camera, OnTakePictureFailed(Eq(CameraResult::kError), + Eq("Failed to take photo"))) + .Times(1); + + capture_controller->TakePicture("mock_path_to_photo"); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; + photo_sink = nullptr; +} + +TEST(CaptureController, ReportsTakePictureAccessDenied) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to take picture + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); + + ComPtr photo_sink = new MockCapturePhotoSink(); + + // Initialize photo sink + MockPhotoSink(engine.Get(), photo_sink.Get()); + + // Cause take picture to fail. + EXPECT_CALL(*(engine.Get()), TakePhoto) + .Times(1) + .WillOnce(Return(E_ACCESSDENIED)); + + EXPECT_CALL(*camera, OnTakePictureSucceeded).Times(0); + EXPECT_CALL(*camera, OnTakePictureFailed(Eq(CameraResult::kAccessDenied), + Eq("Failed to take photo"))) + .Times(1); + + capture_controller->TakePicture("mock_path_to_photo"); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; + photo_sink = nullptr; +} + +TEST(CaptureController, ReportsPhotoTakenErrorEvent) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to take picture + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); + + ComPtr photo_sink = new MockCapturePhotoSink(); + + // Initialize photo sink + MockPhotoSink(engine.Get(), photo_sink.Get()); + + // Request photo + std::string mock_path_to_photo = "mock_path_to_photo"; + EXPECT_CALL(*(engine.Get()), TakePhoto()).Times(1).WillOnce(Return(S_OK)); + capture_controller->TakePicture(mock_path_to_photo); + + // Send take picture failed event + EXPECT_CALL(*camera, OnTakePictureSucceeded).Times(0); + EXPECT_CALL(*camera, OnTakePictureFailed(Eq(CameraResult::kError), + Eq("Unspecified error"))) + .Times(1); + + engine->CreateFakeEvent(E_FAIL, MF_CAPTURE_ENGINE_PHOTO_TAKEN); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; + photo_sink = nullptr; +} + +TEST(CaptureController, ReportsPhotoTakenAccessDeniedEvent) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to take picture + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + ComPtr capture_source = new MockCaptureSource(); + + // Prepare fake media types + MockAvailableMediaTypes(engine.Get(), capture_source.Get(), 1, 1); + + ComPtr photo_sink = new MockCapturePhotoSink(); + + // Initialize photo sink + MockPhotoSink(engine.Get(), photo_sink.Get()); + + // Request photo + std::string mock_path_to_photo = "mock_path_to_photo"; + EXPECT_CALL(*(engine.Get()), TakePhoto()).Times(1).WillOnce(Return(S_OK)); + capture_controller->TakePicture(mock_path_to_photo); + + // Send take picture failed event + EXPECT_CALL(*camera, OnTakePictureSucceeded).Times(0); + EXPECT_CALL(*camera, OnTakePictureFailed(Eq(CameraResult::kAccessDenied), + Eq("Access is denied."))) + .Times(1); + + engine->CreateFakeEvent(E_ACCESSDENIED, MF_CAPTURE_ENGINE_PHOTO_TAKEN); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; + photo_sink = nullptr; +} + TEST(CaptureController, PauseResumePreviewSuccess) { ComPtr engine = new MockCaptureEngine(); std::unique_ptr camera = @@ -469,23 +1352,21 @@ TEST(CaptureController, PauseResumePreviewSuccess) { std::unique_ptr texture_registrar = std::make_unique(); - uint64_t mock_texture_id = 1234; + int64_t mock_texture_id = 1234; // Initialize capture controller to be able to start preview MockInitCaptureController(capture_controller.get(), texture_registrar.get(), engine.Get(), camera.get(), mock_texture_id); ComPtr preview_sink = new MockCapturePreviewSink(); - ComPtr capture_source = new MockCaptureSource(); std::unique_ptr mock_source_buffer = std::make_unique(0); // Start preview to be able to start record - MockStartPreview(capture_controller.get(), capture_source.Get(), - preview_sink.Get(), texture_registrar.get(), engine.Get(), - camera.get(), std::move(mock_source_buffer), 0, 1, 1, - mock_texture_id); + MockStartPreview(capture_controller.get(), preview_sink.Get(), + texture_registrar.get(), engine.Get(), camera.get(), + std::move(mock_source_buffer), 0, 1, 1, mock_texture_id); EXPECT_CALL(*camera, OnPausePreviewSucceeded()).Times(1); capture_controller->PausePreview(); @@ -499,5 +1380,59 @@ TEST(CaptureController, PauseResumePreviewSuccess) { camera = nullptr; } +TEST(CaptureController, PausePreviewFailsIfPreviewNotStarted) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + // Pause preview fails if not started + EXPECT_CALL(*camera, OnPausePreviewFailed(Eq(CameraResult::kError), + Eq("Preview not started"))) + .Times(1); + + capture_controller->PausePreview(); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; +} + +TEST(CaptureController, ResumePreviewFailsIfPreviewNotStarted) { + ComPtr engine = new MockCaptureEngine(); + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + int64_t mock_texture_id = 1234; + + // Initialize capture controller to be able to start preview + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + // Resume preview fails if not started. + EXPECT_CALL(*camera, OnResumePreviewFailed(Eq(CameraResult::kError), + Eq("Preview not started"))) + .Times(1); + + capture_controller->ResumePreview(); + + capture_controller = nullptr; + texture_registrar = nullptr; + engine = nullptr; + camera = nullptr; +} + } // namespace test } // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/test/mocks.h b/packages/camera/camera_windows/windows/test/mocks.h index 0781989e94c2..b6416eb7c710 100644 --- a/packages/camera/camera_windows/windows/test/mocks.h +++ b/packages/camera/camera_windows/windows/test/mocks.h @@ -67,7 +67,8 @@ class MockTextureRegistrar : public flutter::TextureRegistrar { return this->texture_id_; }); - ON_CALL(*this, UnregisterTexture) + // Deprecated pre-Flutter-3.4 version. + ON_CALL(*this, UnregisterTexture(_)) .WillByDefault([this](int64_t tid) -> bool { if (tid == this->texture_id_) { texture_ = nullptr; @@ -77,6 +78,18 @@ class MockTextureRegistrar : public flutter::TextureRegistrar { return false; }); + // Flutter 3.4+ version. + ON_CALL(*this, UnregisterTexture(_, _)) + .WillByDefault( + [this](int64_t tid, std::function callback) -> void { + // Forward to the pre-3.4 implementation so that expectations can + // be the same for all versions. + this->UnregisterTexture(tid); + if (callback) { + callback(); + } + }); + ON_CALL(*this, MarkTextureFrameAvailable) .WillByDefault([this](int64_t tid) -> bool { if (tid == this->texture_id_) { @@ -91,7 +104,13 @@ class MockTextureRegistrar : public flutter::TextureRegistrar { MOCK_METHOD(int64_t, RegisterTexture, (flutter::TextureVariant * texture), (override)); + // Pre-Flutter-3.4 version. MOCK_METHOD(bool, UnregisterTexture, (int64_t), (override)); + // Flutter 3.4+ version. + // TODO(cbracken): Add an override annotation to this once 3.4+ is the + // minimum version tested in CI. + MOCK_METHOD(void, UnregisterTexture, + (int64_t, std::function callback), ()); MOCK_METHOD(bool, MarkTextureFrameAvailable, (int64_t), (override)); int64_t texture_id_ = -1; @@ -134,41 +153,43 @@ class MockCamera : public Camera { (override)); MOCK_METHOD(std::unique_ptr>, GetPendingResultByType, (PendingResultType type)); - MOCK_METHOD(void, OnCreateCaptureEngineFailed, (const std::string& error), - (override)); + MOCK_METHOD(void, OnCreateCaptureEngineFailed, + (CameraResult result, const std::string& error), (override)); MOCK_METHOD(void, OnStartPreviewSucceeded, (int32_t width, int32_t height), (override)); - MOCK_METHOD(void, OnStartPreviewFailed, (const std::string& error), - (override)); + MOCK_METHOD(void, OnStartPreviewFailed, + (CameraResult result, const std::string& error), (override)); MOCK_METHOD(void, OnResumePreviewSucceeded, (), (override)); - MOCK_METHOD(void, OnResumePreviewFailed, (const std::string& error), - (override)); + MOCK_METHOD(void, OnResumePreviewFailed, + (CameraResult result, const std::string& error), (override)); MOCK_METHOD(void, OnPausePreviewSucceeded, (), (override)); - MOCK_METHOD(void, OnPausePreviewFailed, (const std::string& error), - (override)); + MOCK_METHOD(void, OnPausePreviewFailed, + (CameraResult result, const std::string& error), (override)); MOCK_METHOD(void, OnStartRecordSucceeded, (), (override)); - MOCK_METHOD(void, OnStartRecordFailed, (const std::string& error), - (override)); + MOCK_METHOD(void, OnStartRecordFailed, + (CameraResult result, const std::string& error), (override)); MOCK_METHOD(void, OnStopRecordSucceeded, (const std::string& file_path), (override)); - MOCK_METHOD(void, OnStopRecordFailed, (const std::string& error), (override)); + MOCK_METHOD(void, OnStopRecordFailed, + (CameraResult result, const std::string& error), (override)); MOCK_METHOD(void, OnTakePictureSucceeded, (const std::string& file_path), (override)); - MOCK_METHOD(void, OnTakePictureFailed, (const std::string& error), - (override)); + MOCK_METHOD(void, OnTakePictureFailed, + (CameraResult result, const std::string& error), (override)); MOCK_METHOD(void, OnVideoRecordSucceeded, (const std::string& file_path, int64_t video_duration), (override)); - MOCK_METHOD(void, OnVideoRecordFailed, (const std::string& error), - (override)); - MOCK_METHOD(void, OnCaptureError, (const std::string& error), (override)); + MOCK_METHOD(void, OnVideoRecordFailed, + (CameraResult result, const std::string& error), (override)); + MOCK_METHOD(void, OnCaptureError, + (CameraResult result, const std::string& error), (override)); MOCK_METHOD(bool, HasDeviceId, (std::string & device_id), (const override)); MOCK_METHOD(bool, HasCameraId, (int64_t camera_id), (const override)); @@ -182,7 +203,7 @@ class MockCamera : public Camera { MOCK_METHOD(camera_windows::CaptureController*, GetCaptureController, (), (override)); - MOCK_METHOD(void, InitCamera, + MOCK_METHOD(bool, InitCamera, (flutter::TextureRegistrar * texture_registrar, flutter::BinaryMessenger* messenger, bool record_audio, ResolutionPreset resolution_preset), @@ -212,7 +233,7 @@ class MockCaptureController : public CaptureController { public: ~MockCaptureController() = default; - MOCK_METHOD(void, InitCaptureDevice, + MOCK_METHOD(bool, InitCaptureDevice, (flutter::TextureRegistrar * texture_registrar, const std::string& device_id, bool record_audio, ResolutionPreset resolution_preset), diff --git a/packages/e2e/README.md b/packages/e2e/README.md index e86126e4cc56..89c81f8c6e27 100644 --- a/packages/e2e/README.md +++ b/packages/e2e/README.md @@ -1,3 +1,3 @@ # e2e (deprecated) -This package has been moved to [integration_test](https://github.com/flutter/plugins/tree/master/packages/integration_test). +This package has been moved to [`integration_test` in the Flutter SDK](https://github.com/flutter/flutter/tree/master/packages/integration_test). diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md index 3f19984b9437..38726df01fa6 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,6 +1,31 @@ -## NEXT +## 0.2.0+5 + +* Updates android gradle plugin to 7.3.1. + +## 0.2.0+4 + +* Updates minimum Flutter version to 2.10. +* Bumps gson to 2.9.1 + +## 0.2.0+3 + +* Bumps okhttp to 4.10.0. + +## 0.2.0+2 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.2.0+1 + +* Adds OS version support information to README. +* Updates `androidx.test.ext:junit` and `androidx.test.ext:truth` for + compatibility with updated Flutter template. + +## 0.2.0 * Updates compileSdkVersion to 31. +* **Breaking Change** Update guava version to latest stable: `com.google.guava:guava:31.1-android`. ## 0.1.0+4 diff --git a/packages/espresso/README.md b/packages/espresso/README.md index 7747560682e9..95c72e334423 100644 --- a/packages/espresso/README.md +++ b/packages/espresso/README.md @@ -2,6 +2,10 @@ Provides bindings for Espresso tests of Flutter Android apps. +| | Android | +|-------------|---------| +| **Support** | SDK 16+ | + ## Installation Add the `espresso` package as a `dev_dependency` in your app's pubspec.yaml. If you're testing the example app of a package, add it as a dev_dependency of the main package as well. @@ -14,7 +18,7 @@ Add the following dependencies in android/app/build.gradle: ```groovy dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' testImplementation "com.google.truth:truth:1.0" androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' @@ -81,13 +85,13 @@ void main() { The following command line command runs the test locally: -``` +```sh ./gradlew app:connectedAndroidTest -Ptarget=`pwd`/../test_driver/example.dart ``` Espresso tests can also be run on [Firebase Test Lab](https://firebase.google.com/docs/test-lab): -``` +```sh ./gradlew app:assembleAndroidTest ./gradlew app:assembleDebug -Ptarget=.dart gcloud auth activate-service-account --key-file= @@ -99,4 +103,3 @@ gcloud firebase test android run --type instrumentation \ --results-bucket= \ --results-dir= ``` - diff --git a/packages/espresso/android/build.gradle b/packages/espresso/android/build.gradle index e5a042095c2f..ce461b1ebe55 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:3.5.0' + classpath 'com.android.tools.build:gradle:7.3.1' } } @@ -29,6 +29,8 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { + // TODO(stuartmorgan): Enable when gradle is updated. + // disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' baseline file("lint-baseline.xml") @@ -49,12 +51,12 @@ android { } dependencies { - implementation 'com.google.guava:guava:28.1-android' - implementation 'com.squareup.okhttp3:okhttp:3.12.1' - implementation 'com.google.code.gson:gson:2.8.6' + 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' androidTestImplementation 'org.hamcrest:hamcrest:2.2' - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' testImplementation "com.google.truth:truth:1.0" api 'androidx.test:runner:1.1.1' api 'androidx.test.espresso:espresso-core:3.1.1' @@ -67,8 +69,8 @@ dependencies { api 'androidx.test:rules:1.1.0' // Assertions - api 'androidx.test.ext:junit:1.0.0' - api 'androidx.test.ext:truth:1.0.0' + api 'androidx.test.ext:junit:1.1.3' + api 'androidx.test.ext:truth:1.4.0' api 'com.google.truth:truth:0.42' // Espresso dependencies diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java index a1cdd977066c..a8ddfc6bb5eb 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java @@ -42,7 +42,7 @@ * An implementation of the Espresso-Flutter testing protocol by using the testing APIs exposed by * Dart VM service protocol. * - * @see Dart VM + * @see Dart VM * Service Protocol. */ public final class DartVmService implements FlutterTestingProtocol { diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetVmResponse.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetVmResponse.java index 94cac364ddc7..0f4815cd2571 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetVmResponse.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetVmResponse.java @@ -15,7 +15,7 @@ /** * Represents a response of a getVM() + * href="https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md#getvm">getVM() * request. */ public class GetVmResponse { diff --git a/packages/espresso/example/README.md b/packages/espresso/example/README.md index 224544e9f83f..edb498a11338 100644 --- a/packages/espresso/example/README.md +++ b/packages/espresso/example/README.md @@ -8,7 +8,7 @@ The espresso package only runs tests on Android. The example runs on iOS, but th To run the Espresso tests: -``` +```java flutter build apk --debug ./gradlew app:connectedAndroidTest ``` diff --git a/packages/espresso/example/android/app/build.gradle b/packages/espresso/example/android/app/build.gradle index 5ed5eedb0175..21a59edcba40 100644 --- a/packages/espresso/example/android/app/build.gradle +++ b/packages/espresso/example/android/app/build.gradle @@ -55,7 +55,7 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' testImplementation "com.google.truth:truth:1.0" androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' diff --git a/packages/espresso/example/android/build.gradle b/packages/espresso/example/android/build.gradle index 456d020f6e2c..c21bff8e0a2f 100644 --- a/packages/espresso/example/android/build.gradle +++ b/packages/espresso/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.0.1' } } diff --git a/packages/espresso/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/espresso/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146b7318..b8793d3c0d69 100644 --- a/packages/espresso/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/espresso/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-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/espresso/example/lib/main.dart b/packages/espresso/example/lib/main.dart index 14f94abb28c8..741cd9cf9fa2 100644 --- a/packages/espresso/example/lib/main.dart +++ b/packages/espresso/example/lib/main.dart @@ -4,10 +4,13 @@ import 'package:flutter/material.dart'; -void main() => runApp(MyApp()); +void main() => runApp(const MyApp()); /// Example app for Espresso plugin. class MyApp extends StatelessWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + // This widget is the root of your application. @override Widget build(BuildContext context) { @@ -45,7 +48,7 @@ class _MyHomePage extends StatefulWidget { final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State<_MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<_MyHomePage> { diff --git a/packages/espresso/example/pubspec.yaml b/packages/espresso/example/pubspec.yaml index 6a5fcdd466fe..67f9edcd4644 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: ">=1.20.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -22,7 +22,6 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/espresso/pubspec.yaml b/packages/espresso/pubspec.yaml index 1836e7afb575..16fcbfb3aa2c 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.1.0+4 +version: 0.2.0+5 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: @@ -23,4 +23,3 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/file_selector/file_selector/AUTHORS b/packages/file_selector/file_selector/AUTHORS index dbf9d190931b..94743a9a64ae 100644 --- a/packages/file_selector/file_selector/AUTHORS +++ b/packages/file_selector/file_selector/AUTHORS @@ -63,3 +63,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +TowaYamashita diff --git a/packages/file_selector/file_selector/CHANGELOG.md b/packages/file_selector/file_selector/CHANGELOG.md index 65c41651935c..7983aa57561f 100644 --- a/packages/file_selector/file_selector/CHANGELOG.md +++ b/packages/file_selector/file_selector/CHANGELOG.md @@ -1,3 +1,54 @@ +## 0.9.2+2 + +* Improves API docs and examples. +* Changes XTypeGroup initialization from final to const. +* Updates minimum Flutter version to 2.10. + +## 0.9.2 + +* Adds an endorsed iOS implementation. + +## 0.9.1 + +* Adds an endorsed Linux implementation. + +## 0.9.0 + +* **BREAKING CHANGE**: The following methods: + * `openFile` + * `openFiles` + * `getSavePath` + + can throw `ArgumentError`s if called with any `XTypeGroup`s that + do not contain appropriate filters for the current platform. For + example, an `XTypeGroup` that only specifies `webWildCards` will + throw on non-web platforms. + + To avoid runtime errors, ensure that all `XTypeGroup`s (other than + wildcards) set filters that cover every platform your application + targets. See the README for details. + +## 0.8.4+3 + +* Improves API docs and examples. +* Minor fixes for new analysis options. + +## 0.8.4+2 + +* Removes unnecessary imports. +* Adds OS version support information to README. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.8.4+1 + +* Adds README information about macOS entitlements. +* Adds necessary entitlement to macOS example. + +## 0.8.4 + +* Adds an endorsed macOS implementation. + ## 0.8.3 * Adds an endorsed Windows implementation. diff --git a/packages/file_selector/file_selector/README.md b/packages/file_selector/file_selector/README.md index 22ae7073ca2d..938e796b879c 100644 --- a/packages/file_selector/file_selector/README.md +++ b/packages/file_selector/file_selector/README.md @@ -1,36 +1,121 @@ # file_selector + + [![pub package](https://img.shields.io/pub/v/file_selector.svg)](https://pub.dartlang.org/packages/file_selector) A Flutter plugin that manages files and interactions with file dialogs. +| | iOS | Linux | macOS | Web | Windows | +|-------------|--------|-------|--------|-----|-------------| +| **Support** | iOS 9+ | Any | 10.11+ | Any | Windows 10+ | + ## Usage + To use this plugin, add `file_selector` as a [dependency in your pubspec.yaml file](https://flutter.dev/platform-plugins/). +### macOS + +You will need to [add an entitlement][entitlement] for either read-only access: +```xml + com.apple.security.files.user-selected.read-only + +``` +or read/write access: +```xml + com.apple.security.files.user-selected.read-write + +``` +depending on your use case. + ### Examples -Here are small examples that show you how to use the API. + +Here are small examples that show you how to use the API. Please also take a look at our [example][example] app. #### Open a single file + + ``` dart -final typeGroup = XTypeGroup(label: 'images', extensions: ['jpg', 'png']); -final file = await openFile(acceptedTypeGroups: [typeGroup]); +const XTypeGroup typeGroup = XTypeGroup( + label: 'images', + extensions: ['jpg', 'png'], +); +final XFile? file = + await openFile(acceptedTypeGroups: [typeGroup]); ``` #### Open multiple files at once + + ``` dart -final typeGroup = XTypeGroup(label: 'images', extensions: ['jpg', 'png']); -final files = await openFiles(acceptedTypeGroups: [typeGroup]); +const XTypeGroup jpgsTypeGroup = XTypeGroup( + label: 'JPEGs', + extensions: ['jpg', 'jpeg'], +); +const XTypeGroup pngTypeGroup = XTypeGroup( + label: 'PNGs', + extensions: ['png'], +); +final List files = await openFiles(acceptedTypeGroups: [ + jpgsTypeGroup, + pngTypeGroup, +]); +``` + +#### Save a file + + +```dart +const String fileName = 'suggested_name.txt'; +final String? path = await getSavePath(suggestedName: fileName); +if (path == null) { + // Operation was canceled by the user. + return; +} + +final Uint8List fileData = Uint8List.fromList('Hello World!'.codeUnits); +const String mimeType = 'text/plain'; +final XFile textFile = + XFile.fromData(fileData, mimeType: mimeType, name: fileName); +await textFile.saveTo(path); ``` -#### Saving a file +#### Get a directory path + + ```dart -final path = await getSavePath(); -final name = "hello_file_selector.txt"; -final data = Uint8List.fromList("Hello World!".codeUnits); -final mimeType = "text/plain"; -final file = XFile.fromData(data, name: name, mimeType: mimeType); -await file.saveTo(path); +final String? directoryPath = await getDirectoryPath(); +if (directoryPath == null) { + // Operation was canceled by the user. + return; +} ``` +### Filtering by file types + +Different platforms support different type group filter options. To avoid +`ArgumentError`s on some platforms, ensure that any `XTypeGroup`s you pass set +filters that cover all platforms you are targeting, or that you conditionally +pass different `XTypeGroup`s based on `Platform`. + +| | Linux | macOS | Web | Windows | +|----------------|-------|--------|-----|-------------| +| `extensions` | ✔️ | ✔️ | ✔️ | ✔️ | +| `mimeTypes` | ✔️ | ✔️† | ✔️ | | +| `macUTIs` | | ✔️ | | | +| `webWildCards` | | | ✔️ | | + +† `mimeTypes` are not supported on version of macOS earlier than 11 (Big Sur). + +### Features supported by platform + +| Feature | Description | iOS | Linux | macOS | Windows | Web | +| ---------------------- |----------------------------------- |--------- | ---------- | -------- | ------------ | ----------- | +| Choose a single file | Pick a file/image | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| Choose multiple files | Pick multiple files/images | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| Choose a save location | Pick a directory to save a file in | ❌ | ✔️ | ✔️ | ✔️ | ❌ | +| Choose a directory | Pick a folder and get its path | ❌ | ✔️ | ✔️ | ✔️ | ❌ | + [example]:./example +[entitlement]: https://docs.flutter.dev/desktop#entitlements-and-the-app-sandbox \ No newline at end of file diff --git a/packages/file_selector/file_selector/example/.gitignore b/packages/file_selector/file_selector/example/.gitignore index 7abd0753cfc3..f3c205341e7d 100644 --- a/packages/file_selector/file_selector/example/.gitignore +++ b/packages/file_selector/file_selector/example/.gitignore @@ -40,9 +40,5 @@ app.*.symbols # Obfuscation related app.*.map.json -# Currently only web supported -android/ -ios/ - # Exceptions to above rules. !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/file_selector/file_selector/example/README.md b/packages/file_selector/file_selector/example/README.md index 93260dc716b2..e1dcf70473c9 100644 --- a/packages/file_selector/file_selector/example/README.md +++ b/packages/file_selector/file_selector/example/README.md @@ -1,8 +1,3 @@ # file_selector_example Demonstrates how to use the file_selector plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/file_selector/file_selector/example/build.excerpt.yaml b/packages/file_selector/file_selector/example/build.excerpt.yaml new file mode 100644 index 000000000000..e317efa11cb3 --- /dev/null +++ b/packages/file_selector/file_selector/example/build.excerpt.yaml @@ -0,0 +1,15 @@ +targets: + $default: + sources: + include: + - lib/** + # Some default includes that aren't really used here but will prevent + # false-negative warnings: + - $package$ + - lib/$lib$ + exclude: + - '**/.*/**' + - '**/build/**' + builders: + code_excerpter|code_excerpter: + enabled: true diff --git a/packages/file_selector/file_selector/example/ios/.gitignore b/packages/file_selector/file_selector/example/ios/.gitignore new file mode 100644 index 000000000000..7a7f9873ad7d --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/file_selector/file_selector/example/ios/Flutter/AppFrameworkInfo.plist b/packages/file_selector/file_selector/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..9625e105df39 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/packages/file_selector/file_selector/example/ios/Flutter/Debug.xcconfig b/packages/file_selector/file_selector/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..ec97fc6f3021 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/file_selector/file_selector/example/ios/Flutter/Release.xcconfig b/packages/file_selector/file_selector/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..c4855bfe2000 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/file_selector/file_selector/example/ios/Podfile b/packages/file_selector/file_selector/example/ios/Podfile new file mode 100644 index 000000000000..88359b225fa1 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/project.pbxproj b/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..fe3d67b222fe --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,483 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + 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 */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..c87d15a33520 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/file_selector/file_selector/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..1d526a16ed0f --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/file_selector/file_selector/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/file_selector/file_selector/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/file_selector/file_selector/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/file_selector/file_selector/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/file_selector/file_selector/example/ios/Runner/AppDelegate.swift b/packages/file_selector/file_selector/example/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000000..caf998393333 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,17 @@ +// 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 UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000000..dc9ada4725e9 Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..7353c41ecf9c Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..797d452e4589 Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..6ed2d933e112 Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cd7b0099ca8 Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..fe730945a01f Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..321773cd857a Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..797d452e4589 Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..502f463a9bc8 Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..0ec303439225 Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..0ec303439225 Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..e9f5fea27c70 Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..84ac32ae7d98 Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..8953cba09064 Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..0467bf12aa4d Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/file_selector/file_selector/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/file_selector/file_selector/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector/example/ios/Runner/Base.lproj/Main.storyboard b/packages/file_selector/file_selector/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector/example/ios/Runner/Info.plist b/packages/file_selector/file_selector/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..7f553465b77e --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/packages/file_selector/file_selector/example/ios/Runner/Runner-Bridging-Header.h b/packages/file_selector/file_selector/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000000..eb7e8ba8052f --- /dev/null +++ b/packages/file_selector/file_selector/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1,5 @@ +// 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 "GeneratedPluginRegistrant.h" 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 b3ed9d0eeaca..de80aa56be56 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 @@ -3,10 +3,16 @@ // found in the LICENSE file. import 'package:file_selector/file_selector.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// Screen that shows an example of getDirectoryPath class GetDirectoryPage extends StatelessWidget { + /// Default Constructor + GetDirectoryPage({Key? key}) : super(key: key); + + final bool _isIOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; + Future _getDirectoryPath(BuildContext context) async { const String confirmButtonText = 'Choose'; final String? directoryPath = await getDirectoryPath( @@ -34,11 +40,16 @@ class GetDirectoryPage extends StatelessWidget { 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 a directory'), - onPressed: () => _getDirectoryPath(context), + onPressed: _isIOS ? null : () => _getDirectoryPath(context), + child: const Text( + 'Press to ask user to choose a directory (not supported on iOS).', + ), ), ], ), @@ -50,7 +61,7 @@ class GetDirectoryPage extends StatelessWidget { /// Widget that displays a text file in a dialog class TextDisplay extends StatelessWidget { /// Default Constructor - const TextDisplay(this.directoryPath); + const TextDisplay(this.directoryPath, {Key? key}) : super(key: key); /// Directory path final String directoryPath; diff --git a/packages/file_selector/file_selector/example/lib/home_page.dart b/packages/file_selector/file_selector/example/lib/home_page.dart index c598cbdf2611..7b4582c5f5e3 100644 --- a/packages/file_selector/file_selector/example/lib/home_page.dart +++ b/packages/file_selector/file_selector/example/lib/home_page.dart @@ -6,10 +6,16 @@ import 'package:flutter/material.dart'; /// Home Page of the application class HomePage extends StatelessWidget { + /// Default Constructor + const HomePage({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { final ButtonStyle 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, ); return Scaffold( diff --git a/packages/file_selector/file_selector/example/lib/main.dart b/packages/file_selector/file_selector/example/lib/main.dart index 14ce3f593f33..a15842a1191c 100644 --- a/packages/file_selector/file_selector/example/lib/main.dart +++ b/packages/file_selector/file_selector/example/lib/main.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 'package:example/get_directory_page.dart'; -import 'package:example/home_page.dart'; -import 'package:example/open_image_page.dart'; -import 'package:example/open_multiple_images_page.dart'; -import 'package:example/open_text_page.dart'; -import 'package:example/save_text_page.dart'; import 'package:flutter/material.dart'; +import 'get_directory_page.dart'; +import 'home_page.dart'; +import 'open_image_page.dart'; +import 'open_multiple_images_page.dart'; +import 'open_text_page.dart'; +import 'save_text_page.dart'; + void main() { - runApp(MyApp()); + runApp(const MyApp()); } /// MyApp is the Main Application class MyApp extends StatelessWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -24,11 +28,12 @@ class MyApp extends StatelessWidget { primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: HomePage(), + home: const HomePage(), routes: { - '/open/image': (BuildContext context) => OpenImagePage(), - '/open/images': (BuildContext context) => OpenMultipleImagesPage(), - '/open/text': (BuildContext context) => OpenTextPage(), + '/open/image': (BuildContext context) => const OpenImagePage(), + '/open/images': (BuildContext context) => + const OpenMultipleImagesPage(), + '/open/text': (BuildContext context) => const OpenTextPage(), '/save/text': (BuildContext context) => SaveTextPage(), '/directory': (BuildContext context) => GetDirectoryPage(), }, diff --git a/packages/file_selector/file_selector/example/lib/open_image_page.dart b/packages/file_selector/file_selector/example/lib/open_image_page.dart index 0abdba6eb72d..ba18e6e78594 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 @@ -10,18 +10,22 @@ import 'package:flutter/material.dart'; /// Screen that shows an example of openFiles class OpenImagePage extends StatelessWidget { + /// Default Constructor + const OpenImagePage({Key? key}) : super(key: key); + Future _openImageFile(BuildContext context) async { - final XTypeGroup typeGroup = XTypeGroup( + // #docregion SingleOpen + const XTypeGroup typeGroup = XTypeGroup( label: 'images', extensions: ['jpg', 'png'], ); - final List files = - await openFiles(acceptedTypeGroups: [typeGroup]); - if (files.isEmpty) { + final XFile? file = + await openFile(acceptedTypeGroups: [typeGroup]); + // #enddocregion SingleOpen + if (file == null) { // Operation was canceled by the user. return; } - final XFile file = files[0]; final String fileName = file.name; final String filePath = file.path; @@ -43,7 +47,10 @@ class OpenImagePage extends StatelessWidget { 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 open an image file(png, jpg)'), @@ -59,7 +66,8 @@ class OpenImagePage extends StatelessWidget { /// Widget that displays a text file in a dialog class ImageDisplay extends StatelessWidget { /// Default Constructor - const ImageDisplay(this.fileName, this.filePath); + const ImageDisplay(this.fileName, this.filePath, {Key? key}) + : super(key: key); /// Image's name final String fileName; 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 9a1101214aaa..8ae83c2a85dc 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 @@ -10,12 +10,16 @@ import 'package:flutter/material.dart'; /// Screen that shows an example of openFiles class OpenMultipleImagesPage extends StatelessWidget { + /// Default Constructor + const OpenMultipleImagesPage({Key? key}) : super(key: key); + Future _openImageFile(BuildContext context) async { - final XTypeGroup jpgsTypeGroup = XTypeGroup( + // #docregion MultiOpen + const XTypeGroup jpgsTypeGroup = XTypeGroup( label: 'JPEGs', extensions: ['jpg', 'jpeg'], ); - final XTypeGroup pngTypeGroup = XTypeGroup( + const XTypeGroup pngTypeGroup = XTypeGroup( label: 'PNGs', extensions: ['png'], ); @@ -23,6 +27,7 @@ class OpenMultipleImagesPage extends StatelessWidget { jpgsTypeGroup, pngTypeGroup, ]); + // #enddocregion MultiOpen if (files.isEmpty) { // Operation was canceled by the user. return; @@ -45,7 +50,10 @@ class OpenMultipleImagesPage extends StatelessWidget { 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 open multiple images (png, jpg)'), @@ -61,7 +69,7 @@ class OpenMultipleImagesPage extends StatelessWidget { /// Widget that displays a text file in a dialog class MultipleImagesDisplay extends StatelessWidget { /// Default Constructor - const MultipleImagesDisplay(this.files); + const MultipleImagesDisplay(this.files, {Key? key}) : super(key: key); /// The files containing the images final List files; 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 652e8596cf81..f052db1eefc1 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 @@ -4,16 +4,27 @@ import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; /// Screen that shows an example of openFile class OpenTextPage extends StatelessWidget { + /// Default Constructor + const OpenTextPage({Key? key}) : super(key: key); + Future _openTextFile(BuildContext context) async { - final XTypeGroup typeGroup = XTypeGroup( + const XTypeGroup typeGroup = XTypeGroup( label: 'text', extensions: ['txt', 'json'], ); - final XFile? file = - await openFile(acceptedTypeGroups: [typeGroup]); + // This demonstrates using an initial directory for the prompt, which should + // only be done in cases where the application can likely predict where the + // file would be. In most cases, this parameter should not be provided. + final String initialDirectory = + (await getApplicationDocumentsDirectory()).path; + final XFile? file = await openFile( + acceptedTypeGroups: [typeGroup], + initialDirectory: initialDirectory, + ); if (file == null) { // Operation was canceled by the user. return; @@ -39,7 +50,10 @@ class OpenTextPage extends StatelessWidget { 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 open a text file (json, txt)'), @@ -55,7 +69,8 @@ class OpenTextPage extends StatelessWidget { /// Widget that displays a text file in a dialog class TextDisplay extends StatelessWidget { /// Default Constructor - const TextDisplay(this.fileName, this.fileContent); + const TextDisplay(this.fileName, this.fileContent, {Key? key}) + : super(key: key); /// File's name final String fileName; diff --git a/packages/file_selector/file_selector/example/lib/readme_standalone_excerpts.dart b/packages/file_selector/file_selector/example/lib/readme_standalone_excerpts.dart new file mode 100644 index 000000000000..f8126045019a --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/readme_standalone_excerpts.dart @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file exists solely to host compiled excerpts for README.md, and is not +// intended for use as an actual example application. + +// ignore_for_file: public_member_api_docs + +// 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:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('README snippet app'), + ), + body: const Text('See example in main.dart'), + ), + ); + } + + Future saveFile() async { + // #docregion Save + const String fileName = 'suggested_name.txt'; + final String? path = await getSavePath(suggestedName: fileName); + if (path == null) { + // Operation was canceled by the user. + return; + } + + final Uint8List fileData = Uint8List.fromList('Hello World!'.codeUnits); + const String mimeType = 'text/plain'; + final XFile textFile = + XFile.fromData(fileData, mimeType: mimeType, name: fileName); + await textFile.saveTo(path); + // #enddocregion Save + } + + Future directoryPath() async { + // #docregion GetDirectory + final String? directoryPath = await getDirectoryPath(); + if (directoryPath == null) { + // Operation was canceled by the user. + return; + } + // #enddocregion GetDirectory + } +} diff --git a/packages/file_selector/file_selector/example/lib/save_text_page.dart b/packages/file_selector/file_selector/example/lib/save_text_page.dart index 108ef89b0248..6dc765f7accf 100644 --- a/packages/file_selector/file_selector/example/lib/save_text_page.dart +++ b/packages/file_selector/file_selector/example/lib/save_text_page.dart @@ -2,27 +2,47 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// TODO(tarrinneal): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) +// ignore: unnecessary_import import 'dart:typed_data'; + import 'package:file_selector/file_selector.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; /// Page for showing an example of saving with file_selector class SaveTextPage extends StatelessWidget { + /// Default Constructor + SaveTextPage({Key? key}) : super(key: key); + + final bool _isIOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; + final TextEditingController _nameController = TextEditingController(); final TextEditingController _contentController = TextEditingController(); Future _saveFile() async { - final String? path = await getSavePath(); + final String fileName = _nameController.text; + // This demonstrates using an initial directory for the prompt, which should + // only be done in cases where the application can likely predict where the + // file will be saved. In most cases, this parameter should not be provided. + final String initialDirectory = + (await getApplicationDocumentsDirectory()).path; + final String? path = await getSavePath( + initialDirectory: initialDirectory, + suggestedName: fileName, + ); if (path == null) { // Operation was canceled by the user. return; } + final String text = _contentController.text; - final String fileName = _nameController.text; final Uint8List fileData = Uint8List.fromList(text.codeUnits); const String fileMimeType = 'text/plain'; final XFile textFile = XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); + await textFile.saveTo(path); } @@ -61,11 +81,16 @@ class SaveTextPage extends StatelessWidget { const SizedBox(height: 10), 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 save a text file'), - onPressed: () => _saveFile(), + onPressed: _isIOS ? null : () => _saveFile(), + child: const Text( + 'Press to save a text file (not supported on iOS).', + ), ), ], ), diff --git a/packages/file_selector/file_selector/example/linux/.gitignore b/packages/file_selector/file_selector/example/linux/.gitignore new file mode 100644 index 000000000000..d3896c98444f --- /dev/null +++ b/packages/file_selector/file_selector/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/file_selector/file_selector/example/linux/CMakeLists.txt b/packages/file_selector/file_selector/example/linux/CMakeLists.txt new file mode 100644 index 000000000000..39bed64e6674 --- /dev/null +++ b/packages/file_selector/file_selector/example/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "dev.flutter.plugins.file_selector_linux_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/file_selector/file_selector/example/linux/flutter/CMakeLists.txt b/packages/file_selector/file_selector/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000000..d5bd01648a96 --- /dev/null +++ b/packages/file_selector/file_selector/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/file_selector/file_selector/example/linux/flutter/generated_plugins.cmake b/packages/file_selector/file_selector/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..2db3c22ae228 --- /dev/null +++ b/packages/file_selector/file_selector/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/file_selector/file_selector/example/linux/main.cc b/packages/file_selector/file_selector/example/linux/main.cc new file mode 100644 index 000000000000..1507d02825e7 --- /dev/null +++ b/packages/file_selector/file_selector/example/linux/main.cc @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/file_selector/file_selector/example/linux/my_application.cc b/packages/file_selector/file_selector/example/linux/my_application.cc new file mode 100644 index 000000000000..3a67810f5612 --- /dev/null +++ b/packages/file_selector/file_selector/example/linux/my_application.cc @@ -0,0 +1,111 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/packages/file_selector/file_selector/example/linux/my_application.h b/packages/file_selector/file_selector/example/linux/my_application.h new file mode 100644 index 000000000000..6e9f0c3ff665 --- /dev/null +++ b/packages/file_selector/file_selector/example/linux/my_application.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/file_selector/file_selector/example/macos/.gitignore b/packages/file_selector/file_selector/example/macos/.gitignore new file mode 100644 index 000000000000..746adbb6b9e1 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/file_selector/file_selector/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/file_selector/file_selector/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000000..4b81f9b2d200 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/file_selector/file_selector/example/macos/Flutter/Flutter-Release.xcconfig b/packages/file_selector/file_selector/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000000..5caa9d1579e4 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/file_selector/file_selector/example/macos/Podfile b/packages/file_selector/file_selector/example/macos/Podfile new file mode 100644 index 000000000000..dade8dfad0dc --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/project.pbxproj b/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..c450a1d06cf5 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,632 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 6BA632E5BE2B856B0D473EBF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6D20B684858422917AB21A6 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 17DF935FF296A265D8BE378B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 453A41FF685B9AACDF48F0C6 /* 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 = ""; }; + 6FA96861AA2D76C12832F6C9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + C6D20B684858422917AB21A6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6BA632E5BE2B856B0D473EBF /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 58708F6C9D1522F09C51DA54 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 58708F6C9D1522F09C51DA54 /* Pods */ = { + isa = PBXGroup; + children = ( + 453A41FF685B9AACDF48F0C6 /* Pods-Runner.debug.xcconfig */, + 17DF935FF296A265D8BE378B /* Pods-Runner.release.xcconfig */, + 6FA96861AA2D76C12832F6C9 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C6D20B684858422917AB21A6 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + A778864BDDD7B12C41D66FBB /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 028A8DA36859BD4F05694F96 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 028A8DA36859BD4F05694F96 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + A778864BDDD7B12C41D66FBB /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..fb7259e17785 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/file_selector/file_selector/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/file_selector/file_selector/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/file_selector/file_selector/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/file_selector/file_selector/example/macos/Runner/AppDelegate.swift b/packages/file_selector/file_selector/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000000..5cec4c48f620 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..a2ec33f19f11 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000000..3c4935a7ca84 Binary files /dev/null and b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000000..ed4cc1642168 Binary files /dev/null and b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000000..483be6138973 Binary files /dev/null and b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000000..bcbf36df2f2a Binary files /dev/null and b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000000..9c0a65286476 Binary files /dev/null and b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000000..e71a726136a4 Binary files /dev/null and b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000000..8a31fe2dd3f9 Binary files /dev/null and b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/file_selector/file_selector/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/file_selector/file_selector/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000000..80e867a4e06b --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/file_selector/file_selector/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000000..8b42559e8758 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved. diff --git a/packages/file_selector/file_selector/example/macos/Runner/Configs/Debug.xcconfig b/packages/file_selector/file_selector/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000000..36b0fd9464f4 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/file_selector/file_selector/example/macos/Runner/Configs/Release.xcconfig b/packages/file_selector/file_selector/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000000..dff4f49561c8 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/file_selector/file_selector/example/macos/Runner/Configs/Warnings.xcconfig b/packages/file_selector/file_selector/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000000..42bcbf4780b1 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/file_selector/file_selector/example/macos/Runner/DebugProfile.entitlements b/packages/file_selector/file_selector/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000000..d138bd5b0451 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/packages/file_selector/file_selector/example/macos/Runner/Info.plist b/packages/file_selector/file_selector/example/macos/Runner/Info.plist new file mode 100644 index 000000000000..4789daa6a443 --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/file_selector/file_selector/example/macos/Runner/MainFlutterWindow.swift b/packages/file_selector/file_selector/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000000..32aaeedceb1f --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/file_selector/file_selector/example/macos/Runner/Release.entitlements b/packages/file_selector/file_selector/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000000..19afff14a08c --- /dev/null +++ b/packages/file_selector/file_selector/example/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + + diff --git a/packages/file_selector/file_selector/example/pubspec.yaml b/packages/file_selector/file_selector/example/pubspec.yaml index 531f4790afd0..011d95874ae4 100644 --- a/packages/file_selector/file_selector/example/pubspec.yaml +++ b/packages/file_selector/file_selector/example/pubspec.yaml @@ -1,4 +1,4 @@ -name: example +name: file_selector_example description: A new Flutter project. publish_to: none @@ -17,8 +17,10 @@ dependencies: path: ../ flutter: sdk: flutter + path_provider: ^2.0.9 dev_dependencies: + build_runner: ^2.1.10 flutter_test: sdk: flutter diff --git a/packages/file_selector/file_selector/example/windows/flutter/generated_plugins.cmake b/packages/file_selector/file_selector/example/windows/flutter/generated_plugins.cmake index 63eda9b7b59f..a423a02476a2 100644 --- a/packages/file_selector/file_selector/example/windows/flutter/generated_plugins.cmake +++ b/packages/file_selector/file_selector/example/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/file_selector/file_selector/lib/file_selector.dart b/packages/file_selector/file_selector/lib/file_selector.dart index c2803d60c972..f357af07321a 100644 --- a/packages/file_selector/file_selector/lib/file_selector.dart +++ b/packages/file_selector/file_selector/lib/file_selector.dart @@ -9,7 +9,26 @@ import 'package:file_selector_platform_interface/file_selector_platform_interfac export 'package:file_selector_platform_interface/file_selector_platform_interface.dart' show XFile, XTypeGroup; -/// Open file dialog for loading files and return a file path +/// Opens a file selection dialog and returns the path chosen by the user. +/// +/// [acceptedTypeGroups] is a list of file type groups that can be selected in +/// the dialog. How this is displayed depends on the pltaform, for example: +/// - On Windows and Linux, each group will be an entry in a list of filter +/// options. +/// - On macOS, the union of all types allowed by all of the groups will be +/// allowed. +/// Throws an [ArgumentError] if any type groups do not include filters +/// supported by the current platform. +/// +/// [initialDirectory] is the full path to the directory that will be displayed +/// when the dialog is opened. When not provided, the platform will pick an +/// initial location. This is ignored on the Web platform. +/// +/// [confirmButtonText] is the text in the confirmation button of the dialog. +/// When not provided, the default OS label is used (for example, "Open"). +/// This is ignored on the Web platform. +/// +/// Returns `null` if the user cancels the operation. Future openFile({ List acceptedTypeGroups = const [], String? initialDirectory, @@ -21,7 +40,26 @@ Future openFile({ confirmButtonText: confirmButtonText); } -/// Open file dialog for loading files and return a list of file paths +/// Opens a file selection dialog and returns the list of paths chosen by the +/// user. +/// +/// [acceptedTypeGroups] is a list of file type groups that can be selected in +/// the dialog. How this is displayed depends on the pltaform, for example: +/// - On Windows and Linux, each group will be an entry in a list of filter +/// options. +/// - On macOS, the union of all types allowed by all of the groups will be +/// allowed. +/// Throws an [ArgumentError] if any type groups do not include filters +/// supported by the current platform. +/// +/// [initialDirectory] is the full path to the directory that will be displayed +/// when the dialog is opened. When not provided, the platform will pick an +/// initial location. +/// +/// [confirmButtonText] is the text in the confirmation button of the dialog. +/// When not provided, the default OS label is used (for example, "Open"). +/// +/// Returns an empty list if the user cancels the operation. Future> openFiles({ List acceptedTypeGroups = const [], String? initialDirectory, @@ -33,7 +71,27 @@ Future> openFiles({ confirmButtonText: confirmButtonText); } -/// Saves File to user's file system +/// Opens a save dialog and returns the target path chosen by the user. +/// +/// [acceptedTypeGroups] is a list of file type groups that can be selected in +/// the dialog. How this is displayed depends on the pltaform, for example: +/// - On Windows and Linux, each group will be an entry in a list of filter +/// options. +/// - On macOS, the union of all types allowed by all of the groups will be +/// allowed. +/// Throws an [ArgumentError] if any type groups do not include filters +/// supported by the current platform. +/// +/// [initialDirectory] is the full path to the directory that will be displayed +/// when the dialog is opened. When not provided, the platform will pick an +/// initial location. +/// +/// [suggestedName] is initial value of file name. +/// +/// [confirmButtonText] is the text in the confirmation button of the dialog. +/// When not provided, the default OS label is used (for example, "Save"). +/// +/// Returns `null` if the user cancels the operation. Future getSavePath({ List acceptedTypeGroups = const [], String? initialDirectory, @@ -47,7 +105,17 @@ Future getSavePath({ confirmButtonText: confirmButtonText); } -/// Gets a directory path from a user's file system +/// Opens a directory selection dialog and returns the path chosen by the user. +/// This always returns `null` on the web. +/// +/// [initialDirectory] is the full path to the directory that will be displayed +/// when the dialog is opened. When not provided, the platform will pick an +/// initial location. +/// +/// [confirmButtonText] is the text in the confirmation button of the dialog. +/// When not provided, the default OS label is used (for example, "Open"). +/// +/// Returns `null` if the user cancels the operation. Future getDirectoryPath({ String? initialDirectory, String? confirmButtonText, diff --git a/packages/file_selector/file_selector/pubspec.yaml b/packages/file_selector/file_selector/pubspec.yaml index 41ff35307867..ad187d6f446a 100644 --- a/packages/file_selector/file_selector/pubspec.yaml +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -3,30 +3,38 @@ description: Flutter plugin for opening and saving files, or selecting directories, using native file selection UI. repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.8.3 +version: 0.9.2+2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: platforms: + ios: + default_package: file_selector_ios + linux: + default_package: file_selector_linux + macos: + default_package: file_selector_macos web: default_package: file_selector_web windows: default_package: file_selector_windows dependencies: - file_selector_platform_interface: ^2.0.0 - file_selector_web: ^0.8.1 - file_selector_windows: ^0.8.2 + file_selector_ios: ^0.5.0 + file_selector_linux: ^0.9.0 + file_selector_macos: ^0.9.0 + file_selector_platform_interface: ^2.2.0 + file_selector_web: ^0.9.0 + file_selector_windows: ^0.9.0 flutter: sdk: flutter dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 plugin_platform_interface: ^2.0.0 test: ^1.16.3 diff --git a/packages/file_selector/file_selector/test/file_selector_test.dart b/packages/file_selector/file_selector/test/file_selector_test.dart index 6ab0bd975036..13c986b09922 100644 --- a/packages/file_selector/file_selector/test/file_selector_test.dart +++ b/packages/file_selector/file_selector/test/file_selector_test.dart @@ -6,14 +6,13 @@ import 'package:file_selector/file_selector.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:test/fake.dart'; void main() { late FakeFileSelector fakePlatformImplementation; const String initialDirectory = '/home/flutteruser'; const String confirmButtonText = 'Use this profile picture'; const String suggestedName = 'suggested_name'; - final List acceptedTypeGroups = [ + const List acceptedTypeGroups = [ XTypeGroup(label: 'documents', mimeTypes: [ 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessing', @@ -285,10 +284,12 @@ class FakeFileSelector extends Fake this.confirmButtonText = confirmButtonText; } + // ignore: use_setters_to_change_properties void setFileResponse(List files) { this.files = files; } + // ignore: use_setters_to_change_properties void setPathResponse(String path) { this.path = path; } diff --git a/packages/file_selector/file_selector_ios/.gitignore b/packages/file_selector/file_selector_ios/.gitignore new file mode 100644 index 000000000000..9be145fde98d --- /dev/null +++ b/packages/file_selector/file_selector_ios/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/file_selector/file_selector_ios/.metadata b/packages/file_selector/file_selector_ios/.metadata new file mode 100644 index 000000000000..295d2f7a8803 --- /dev/null +++ b/packages/file_selector/file_selector_ios/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: ac1aa511ca94f46c7e80b94dafd521de35e808e5 + channel: master + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ac1aa511ca94f46c7e80b94dafd521de35e808e5 + base_revision: ac1aa511ca94f46c7e80b94dafd521de35e808e5 + - platform: ios + create_revision: ac1aa511ca94f46c7e80b94dafd521de35e808e5 + base_revision: ac1aa511ca94f46c7e80b94dafd521de35e808e5 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/file_selector/file_selector_ios/AUTHORS b/packages/file_selector/file_selector_ios/AUTHORS new file mode 100644 index 000000000000..557dff97933b --- /dev/null +++ b/packages/file_selector/file_selector_ios/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/packages/file_selector/file_selector_ios/CHANGELOG.md b/packages/file_selector/file_selector_ios/CHANGELOG.md new file mode 100644 index 000000000000..439e1d4fd4c1 --- /dev/null +++ b/packages/file_selector/file_selector_ios/CHANGELOG.md @@ -0,0 +1,12 @@ +## 0.5.0+2 + +* Changes XTypeGroup initialization from final to const. +* Updates minimum Flutter version to 2.10. + +## 0.5.0+1 + +* Updates README for endorsement. + +## 0.5.0 + +* Initial iOS implementation of `file_selector`. diff --git a/packages/file_selector/file_selector_ios/LICENSE b/packages/file_selector/file_selector_ios/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/file_selector/file_selector_ios/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/file_selector/file_selector_ios/README.md b/packages/file_selector/file_selector_ios/README.md new file mode 100644 index 000000000000..4564499e6faf --- /dev/null +++ b/packages/file_selector/file_selector_ios/README.md @@ -0,0 +1,11 @@ +# file\_selector\_ios + +The iOS implementation of [`file_selector`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `file_selector` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/file_selector +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/file_selector/file_selector_ios/example/.gitignore b/packages/file_selector/file_selector_ios/example/.gitignore new file mode 100644 index 000000000000..0fa6b675c0a5 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/file_selector/file_selector_ios/example/.metadata b/packages/file_selector/file_selector_ios/example/.metadata new file mode 100644 index 000000000000..3c3e4b52f734 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 5464c5bac742001448fe4fc0597be939379f88ea + channel: stable + +project_type: app diff --git a/packages/file_selector/file_selector_ios/example/README.md b/packages/file_selector/file_selector_ios/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/file_selector/file_selector_ios/example/ios/.gitignore b/packages/file_selector/file_selector_ios/example/ios/.gitignore new file mode 100644 index 000000000000..7a7f9873ad7d --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/file_selector/file_selector_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/file_selector/file_selector_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..9625e105df39 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/packages/file_selector/file_selector_ios/example/ios/Flutter/Debug.xcconfig b/packages/file_selector/file_selector_ios/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..ec97fc6f3021 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/file_selector/file_selector_ios/example/ios/Flutter/Release.xcconfig b/packages/file_selector/file_selector_ios/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..c4855bfe2000 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/file_selector/file_selector_ios/example/ios/Podfile b/packages/file_selector/file_selector_ios/example/ios/Podfile new file mode 100644 index 000000000000..3c0b3140c95a --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Podfile @@ -0,0 +1,46 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + # Pods for testing + pod 'OCMock', '~> 3.8.1' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..e21f78a55c1b --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,771 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 21160A929DC757957DE39F1E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 000792269CB6B9FE88AC567C /* Pods_Runner.framework */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 6165A2F80DFA224EAF50A1D5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AC3841659BF3693FAC5A2F8F /* Pods_RunnerTests.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + C71AE4C8281C6B6B0086307A /* FileSelectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C71AE4C5281C6B530086307A /* FileSelectorTests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + C71AE4BA281C6A090086307A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 000792269CB6B9FE88AC567C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4A27CC0DB4EF6669B637A1E8 /* 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 = ""; }; + 5667547C6832727A744371E2 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 5C0E87EDCB9350EC4916E293 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 786CCB880423FD6D1019F59B /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 79C120FEED85F112A72B5D35 /* 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; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AC3841659BF3693FAC5A2F8F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C71AE4B6281C6A090086307A /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + C71AE4C5281C6B530086307A /* FileSelectorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FileSelectorTests.m; sourceTree = ""; }; + F818CE2D7CDF8AFF94707327 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 21160A929DC757957DE39F1E /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C71AE4B3281C6A090086307A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6165A2F80DFA224EAF50A1D5 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2E44EE3EE3BCCAB6933171F8 /* Pods */ = { + isa = PBXGroup; + children = ( + 79C120FEED85F112A72B5D35 /* Pods-Runner.debug.xcconfig */, + F818CE2D7CDF8AFF94707327 /* Pods-Runner.release.xcconfig */, + 5C0E87EDCB9350EC4916E293 /* Pods-Runner.profile.xcconfig */, + 4A27CC0DB4EF6669B637A1E8 /* Pods-RunnerTests.debug.xcconfig */, + 5667547C6832727A744371E2 /* Pods-RunnerTests.release.xcconfig */, + 786CCB880423FD6D1019F59B /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + C71AE4C4281C6B370086307A /* RunnerTests */, + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 2E44EE3EE3BCCAB6933171F8 /* Pods */, + C832A34FD3BC866442874ED0 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + C71AE4B6281C6A090086307A /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + C71AE4C4281C6B370086307A /* RunnerTests */ = { + isa = PBXGroup; + children = ( + C71AE4C5281C6B530086307A /* FileSelectorTests.m */, + ); + path = RunnerTests; + sourceTree = ""; + }; + C832A34FD3BC866442874ED0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 000792269CB6B9FE88AC567C /* Pods_Runner.framework */, + AC3841659BF3693FAC5A2F8F /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + AC24910767ED5F17F5245292 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + BE6D85B8F242B768015B938B /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + C71AE4B5281C6A090086307A /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = C71AE4BF281C6A090086307A /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + A68B14611B411E4F96A5C80D /* [CP] Check Pods Manifest.lock */, + C71AE4B2281C6A090086307A /* Sources */, + C71AE4B3281C6A090086307A /* Frameworks */, + C71AE4B4281C6A090086307A /* Resources */, + 5BE5886DAAA885227DE0796D /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + C71AE4BB281C6A090086307A /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = FileSelectorTests; + productReference = C71AE4B6281C6A090086307A /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + C71AE4B5281C6A090086307A = { + CreatedOnToolsVersion = 13.1; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + C71AE4B5281C6A090086307A /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C71AE4B4281C6A090086307A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 5BE5886DAAA885227DE0796D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-RunnerTests/Pods-RunnerTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-RunnerTests/Pods-RunnerTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RunnerTests/Pods-RunnerTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + A68B14611B411E4F96A5C80D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + AC24910767ED5F17F5245292 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + BE6D85B8F242B768015B938B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C71AE4B2281C6A090086307A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C71AE4C8281C6B6B0086307A /* FileSelectorTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + C71AE4BB281C6A090086307A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = C71AE4BA281C6A090086307A /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.fileSelectorIosExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.fileSelectorIosExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.fileSelectorIosExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + C71AE4BC281C6A090086307A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4A27CC0DB4EF6669B637A1E8 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.FileSelectorTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + C71AE4BD281C6A090086307A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5667547C6832727A744371E2 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.FileSelectorTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + C71AE4BE281C6A090086307A /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 786CCB880423FD6D1019F59B /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.FileSelectorTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Profile; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C71AE4BF281C6A090086307A /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C71AE4BC281C6A090086307A /* Debug */, + C71AE4BD281C6A090086307A /* Release */, + C71AE4BE281C6A090086307A /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..c842c6b3214b --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/file_selector/file_selector_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/file_selector/file_selector_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/file_selector/file_selector_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/AppDelegate.swift b/packages/file_selector/file_selector_ios/example/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000000..caf998393333 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,17 @@ +// 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 UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000000..dc9ada4725e9 Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/file_selector/file_selector_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/file_selector/file_selector_ios/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Info.plist b/packages/file_selector/file_selector_ios/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..2bf6e923d3b6 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + File Selector Ios + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + file_selector_ios_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Runner-Bridging-Header.h b/packages/file_selector/file_selector_ios/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000000..eb7e8ba8052f --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1,5 @@ +// 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 "GeneratedPluginRegistrant.h" diff --git a/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.m b/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.m new file mode 100644 index 000000000000..a32622a6afef --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.m @@ -0,0 +1,98 @@ +// 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 file_selector_ios; +@import file_selector_ios.Test; +@import XCTest; + +#import + +@interface FileSelectorTests : XCTestCase + +@end + +@implementation FileSelectorTests + +- (void)testPickerPresents { + FFSFileSelectorPlugin *plugin = [[FFSFileSelectorPlugin alloc] init]; + UIDocumentPickerViewController *picker = + [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[] + inMode:UIDocumentPickerModeImport]; + id mockPresentingVC = OCMClassMock([UIViewController class]); + plugin.documentPickerViewControllerOverride = picker; + plugin.presentingViewControllerOverride = mockPresentingVC; + + [plugin openFileSelectorWithConfig:[FFSFileSelectorConfig makeWithUtis:@[] + allowMultiSelection:@NO] + completion:^(NSArray *paths, FlutterError *error){ + }]; + + XCTAssertEqualObjects(picker.delegate, plugin); + OCMVerify(times(1), [mockPresentingVC presentViewController:picker + animated:[OCMArg any] + completion:[OCMArg any]]); +} + +- (void)testReturnsPickedFiles { + FFSFileSelectorPlugin *plugin = [[FFSFileSelectorPlugin alloc] init]; + XCTestExpectation *completionWasCalled = [self expectationWithDescription:@"completion"]; + UIDocumentPickerViewController *picker = + [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[] + inMode:UIDocumentPickerModeImport]; + plugin.documentPickerViewControllerOverride = picker; + [plugin openFileSelectorWithConfig:[FFSFileSelectorConfig makeWithUtis:@[] + allowMultiSelection:@YES] + completion:^(NSArray *paths, FlutterError *error) { + NSArray *expectedPaths = @[ @"/file1.txt", @"/file2.txt" ]; + XCTAssertEqualObjects(paths, expectedPaths); + [completionWasCalled fulfill]; + }]; + [plugin documentPicker:picker + didPickDocumentsAtURLs:@[ + [NSURL URLWithString:@"file:///file1.txt"], [NSURL URLWithString:@"file:///file2.txt"] + ]]; + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +- (void)testReturnsPickedFileLegacy { + // Tests that it handles the pre iOS 11 UIDocumentPickerDelegate method. + FFSFileSelectorPlugin *plugin = [[FFSFileSelectorPlugin alloc] init]; + XCTestExpectation *completionWasCalled = [self expectationWithDescription:@"completion"]; + UIDocumentPickerViewController *picker = + [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[] + inMode:UIDocumentPickerModeImport]; + plugin.documentPickerViewControllerOverride = picker; + [plugin openFileSelectorWithConfig:[FFSFileSelectorConfig makeWithUtis:@[] + allowMultiSelection:@NO] + completion:^(NSArray *paths, FlutterError *error) { + NSArray *expectedPaths = @[ @"/file1.txt" ]; + XCTAssertEqualObjects(paths, expectedPaths); + [completionWasCalled fulfill]; + }]; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + [plugin documentPicker:picker didPickDocumentAtURL:[NSURL URLWithString:@"file:///file1.txt"]]; +#pragma GCC diagnostic pop + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +- (void)testCancellingPickerReturnsNil { + FFSFileSelectorPlugin *plugin = [[FFSFileSelectorPlugin alloc] init]; + UIDocumentPickerViewController *picker = + [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[] + inMode:UIDocumentPickerModeImport]; + plugin.documentPickerViewControllerOverride = picker; + + XCTestExpectation *completionWasCalled = [self expectationWithDescription:@"completion"]; + [plugin openFileSelectorWithConfig:[FFSFileSelectorConfig makeWithUtis:@[] + allowMultiSelection:@NO] + completion:^(NSArray *paths, FlutterError *error) { + XCTAssertEqual(paths.count, 0); + [completionWasCalled fulfill]; + }]; + [plugin documentPickerWasCancelled:picker]; + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +@end diff --git a/packages/file_selector/file_selector_ios/example/lib/home_page.dart b/packages/file_selector/file_selector_ios/example/lib/home_page.dart new file mode 100644 index 000000000000..7486977556af --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/lib/home_page.dart @@ -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. + +import 'package:flutter/material.dart'; + +/// Home Page of the application. +class HomePage extends StatelessWidget { + /// Default Constructor + const HomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final ButtonStyle 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, + ); + return Scaffold( + appBar: AppBar( + title: const Text('File Selector Demo Home Page'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: style, + child: const Text('Open a text file'), + onPressed: () => Navigator.pushNamed(context, '/open/text'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open an image'), + onPressed: () => Navigator.pushNamed(context, '/open/image'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open multiple images'), + onPressed: () => Navigator.pushNamed(context, '/open/images'), + ), + const SizedBox(height: 10), + ], + ), + ), + ); + } +} diff --git a/packages/file_selector/file_selector_ios/example/lib/main.dart b/packages/file_selector/file_selector_ios/example/lib/main.dart new file mode 100644 index 000000000000..929c48fb9037 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/lib/main.dart @@ -0,0 +1,38 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'home_page.dart'; +import 'open_image_page.dart'; +import 'open_multiple_images_page.dart'; +import 'open_text_page.dart'; + +void main() { + runApp(const MyApp()); +} + +/// MyApp is the Main Application. +class MyApp extends StatelessWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'File Selector Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: const HomePage(), + routes: { + '/open/image': (BuildContext context) => const OpenImagePage(), + '/open/images': (BuildContext context) => + const OpenMultipleImagesPage(), + '/open/text': (BuildContext context) => const OpenTextPage(), + }, + ); + } +} 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 new file mode 100644 index 000000000000..606a64870566 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/lib/open_image_page.dart @@ -0,0 +1,95 @@ +// 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:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select an image file using +/// `openFiles`, then displays the selected images in a gallery dialog. +class OpenImagePage extends StatelessWidget { + /// Default Constructor + const OpenImagePage({Key? key}) : super(key: key); + + Future _openImageFile(BuildContext context) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images', + extensions: ['jpg', 'png'], + macUTIs: ['public.image'], + ); + final XFile? file = await FileSelectorPlatform.instance + .openFile(acceptedTypeGroups: [typeGroup]); + if (file == null) { + // Operation was canceled by the user. + return; + } + final String fileName = file.name; + final String filePath = file.path; + + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open an image'), + ), + 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 open an image file(png, jpg)'), + onPressed: () => _openImageFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays an image in a dialog. +class ImageDisplay extends StatelessWidget { + /// Default Constructor. + const ImageDisplay(this.fileName, this.filePath, {Key? key}) + : super(key: key); + + /// The name of the selected file. + final String fileName; + + /// The path to the selected file. + final String filePath; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: kIsWeb ? Image.network(filePath) : Image.file(File(filePath)), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart new file mode 100644 index 000000000000..adc4a65f12b5 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.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:io'; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select multiple image files using +/// `openFiles`, then displays the selected images in a gallery dialog. +class OpenMultipleImagesPage extends StatelessWidget { + /// Default Constructor + const OpenMultipleImagesPage({Key? key}) : super(key: key); + + Future _openImageFile(BuildContext context) async { + const XTypeGroup jpgsTypeGroup = XTypeGroup( + label: 'JPEGs', + extensions: ['jpg', 'jpeg'], + macUTIs: ['public.jpeg'], + ); + const XTypeGroup pngTypeGroup = XTypeGroup( + label: 'PNGs', + extensions: ['png'], + macUTIs: ['public.png'], + ); + final List files = await FileSelectorPlatform.instance + .openFiles(acceptedTypeGroups: [ + jpgsTypeGroup, + pngTypeGroup, + ]); + if (files.isEmpty) { + // Operation was canceled by the user. + return; + } + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open multiple images'), + ), + 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 open multiple images (png, jpg)'), + onPressed: () => _openImageFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class MultipleImagesDisplay extends StatelessWidget { + /// Default Constructor. + const MultipleImagesDisplay(this.files, {Key? key}) : super(key: key); + + /// The files containing the images. + final List files; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Gallery'), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: Center( + child: Row( + children: [ + ...files.map( + (XFile file) => Flexible( + child: kIsWeb + ? Image.network(file.path) + : Image.file(File(file.path))), + ) + ], + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart b/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart new file mode 100644 index 000000000000..e7bbf8bc937f --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart @@ -0,0 +1,92 @@ +// 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 a text file using `openFile`, then +/// displays its contents in a dialog. +class OpenTextPage extends StatelessWidget { + /// Default Constructor + const OpenTextPage({Key? key}) : super(key: key); + + Future _openTextFile(BuildContext context) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'text', + extensions: ['txt', 'json'], + macUTIs: ['public.text'], + ); + final XFile? file = await FileSelectorPlatform.instance + .openFile(acceptedTypeGroups: [typeGroup]); + if (file == null) { + // Operation was canceled by the user. + return; + } + final String fileName = file.name; + final String fileContent = await file.readAsString(); + + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open a text file'), + ), + 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 open a text file (json, txt)'), + onPressed: () => _openTextFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class TextDisplay extends StatelessWidget { + /// Default Constructor. + const TextDisplay(this.fileName, this.fileContent, {Key? key}) + : super(key: key); + + /// The name of the selected file. + final String fileName; + + /// The contents of the text file. + final String fileContent; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(fileContent), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_ios/example/pubspec.yaml b/packages/file_selector/file_selector_ios/example/pubspec.yaml new file mode 100644 index 000000000000..5a2eaa6f7dcd --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/pubspec.yaml @@ -0,0 +1,31 @@ +name: example +description: Example for file_selector_ios implementation. +publish_to: 'none' +version: 1.0.0 + +environment: + sdk: ">=2.14.4 <3.0.0" + +dependencies: + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + file_selector_ios: + # When depending on this package from a real application you should use: + # file_selector_ios: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: .. + file_selector_platform_interface: ^2.2.0 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true \ No newline at end of file diff --git a/packages/file_selector/file_selector_ios/example/test_driver/integration_test.dart b/packages/file_selector/file_selector_ios/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/test_driver/integration_test.dart @@ -0,0 +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. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/file_selector/file_selector_ios/ios/.gitignore b/packages/file_selector/file_selector_ios/ios/.gitignore new file mode 100644 index 000000000000..0c885071e36b --- /dev/null +++ b/packages/file_selector/file_selector_ios/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Assets/.gitkeep b/packages/file_selector/file_selector_ios/ios/Assets/.gitkeep similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/ios/Assets/.gitkeep rename to packages/file_selector/file_selector_ios/ios/Assets/.gitkeep diff --git a/packages/quick_actions/quick_actions_ios/ios/Classes/FLTQuickActionsPlugin.h b/packages/file_selector/file_selector_ios/ios/Classes/FFSFileSelectorPlugin.h similarity index 76% rename from packages/quick_actions/quick_actions_ios/ios/Classes/FLTQuickActionsPlugin.h rename to packages/file_selector/file_selector_ios/ios/Classes/FFSFileSelectorPlugin.h index 8f98cc35e8ba..ca7ca56f3bd4 100644 --- a/packages/quick_actions/quick_actions_ios/ios/Classes/FLTQuickActionsPlugin.h +++ b/packages/file_selector/file_selector_ios/ios/Classes/FFSFileSelectorPlugin.h @@ -4,5 +4,5 @@ #import -@interface FLTQuickActionsPlugin : NSObject +@interface FFSFileSelectorPlugin : NSObject @end diff --git a/packages/file_selector/file_selector_ios/ios/Classes/FFSFileSelectorPlugin.m b/packages/file_selector/file_selector_ios/ios/Classes/FFSFileSelectorPlugin.m new file mode 100644 index 000000000000..e77585ad3a17 --- /dev/null +++ b/packages/file_selector/file_selector_ios/ios/Classes/FFSFileSelectorPlugin.m @@ -0,0 +1,88 @@ +// 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 "FFSFileSelectorPlugin.h" +#import "FFSFileSelectorPlugin_Test.h" +#import "messages.g.h" + +#import + +@implementation FFSFileSelectorPlugin + +#pragma mark - FFSFileSelectorApi + +- (void)openFileSelectorWithConfig:(FFSFileSelectorConfig *)config + completion:(void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion { + UIDocumentPickerViewController *documentPicker = + self.documentPickerViewControllerOverride + ?: [[UIDocumentPickerViewController alloc] + initWithDocumentTypes:config.utis + inMode:UIDocumentPickerModeImport]; + documentPicker.delegate = self; + if (@available(iOS 11.0, *)) { + documentPicker.allowsMultipleSelection = config.allowMultiSelection.boolValue; + } + + UIViewController *presentingVC = + self.presentingViewControllerOverride + ?: UIApplication.sharedApplication.delegate.window.rootViewController; + if (presentingVC) { + objc_setAssociatedObject(documentPicker, @selector(openFileSelectorWithConfig:completion:), + completion, OBJC_ASSOCIATION_COPY_NONATOMIC); + [presentingVC presentViewController:documentPicker animated:YES completion:nil]; + } else { + completion(nil, [FlutterError errorWithCode:@"error" + message:@"Missing root view controller." + details:nil]); + } +} + +#pragma mark - FlutterPlugin + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FFSFileSelectorPlugin *plugin = [[FFSFileSelectorPlugin alloc] init]; + FFSFileSelectorApiSetup(registrar.messenger, plugin); +} + +#pragma mark - UIDocumentPickerDelegate + +// This method is only called in iOS < 11.0. The new codepath is +// documentPicker:didPickDocumentsAtURLs:, implemented below. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)documentPicker:(UIDocumentPickerViewController *)controller + didPickDocumentAtURL:(NSURL *)url { + [self sendBackResults:@[ url.path ] error:nil forPicker:controller]; +} +#pragma clang diagnostic pop + +- (void)documentPicker:(UIDocumentPickerViewController *)controller + didPickDocumentsAtURLs:(NSArray *)urls { + NSMutableArray *paths = [NSMutableArray arrayWithCapacity:urls.count]; + for (NSURL *url in urls) { + [paths addObject:url.path]; + }; + [self sendBackResults:paths error:nil forPicker:controller]; +} + +- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { + [self sendBackResults:@[] error:nil forPicker:controller]; +} + +#pragma mark - Helper Methods + +- (void)sendBackResults:(NSArray *)results + error:(FlutterError *)error + forPicker:(UIDocumentPickerViewController *)picker { + void (^completionBlock)(NSArray *, FlutterError *) = + objc_getAssociatedObject(picker, @selector(openFileSelectorWithConfig:completion:)); + if (completionBlock) { + completionBlock(results, error); + objc_setAssociatedObject(picker, @selector(openFileSelectorWithConfig:completion:), nil, + OBJC_ASSOCIATION_ASSIGN); + } +} + +@end diff --git a/packages/file_selector/file_selector_ios/ios/Classes/FFSFileSelectorPlugin_Test.h b/packages/file_selector/file_selector_ios/ios/Classes/FFSFileSelectorPlugin_Test.h new file mode 100644 index 000000000000..f71a8ae109e6 --- /dev/null +++ b/packages/file_selector/file_selector_ios/ios/Classes/FFSFileSelectorPlugin_Test.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FFSFileSelectorPlugin.h" + +#import "messages.g.h" + +// This header is available in the Test module. Import via "@import file_selector_ios.Test;". +@interface FFSFileSelectorPlugin () + +/** + * Overrides the view controller used for presenting the document picker. + */ +@property(nonatomic) UIViewController *_Nullable presentingViewControllerOverride; + +/** + * Overrides the UIDocumentPickerViewController used for file picking. + */ +@property(nonatomic) UIDocumentPickerViewController *_Nullable documentPickerViewControllerOverride; + +@end diff --git a/packages/file_selector/file_selector_ios/ios/Classes/FileSelectorPlugin.modulemap b/packages/file_selector/file_selector_ios/ios/Classes/FileSelectorPlugin.modulemap new file mode 100644 index 000000000000..4ff40260ffb3 --- /dev/null +++ b/packages/file_selector/file_selector_ios/ios/Classes/FileSelectorPlugin.modulemap @@ -0,0 +1,10 @@ +framework module file_selector_ios { + umbrella header "file_selector_ios-umbrella.h" + + export * + module * { export * } + + explicit module Test { + header "FFSFileSelectorPlugin_Test.h" + } +} diff --git a/packages/file_selector/file_selector_ios/ios/Classes/file_selector_ios-umbrella.h b/packages/file_selector/file_selector_ios/ios/Classes/file_selector_ios-umbrella.h new file mode 100644 index 000000000000..d79d3642b3e8 --- /dev/null +++ b/packages/file_selector/file_selector_ios/ios/Classes/file_selector_ios-umbrella.h @@ -0,0 +1,6 @@ +// 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 diff --git a/packages/file_selector/file_selector_ios/ios/Classes/messages.g.h b/packages/file_selector/file_selector_ios/ios/Classes/messages.g.h new file mode 100644 index 000000000000..a04b41129a73 --- /dev/null +++ b/packages/file_selector/file_selector_ios/ios/Classes/messages.g.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. +// Autogenerated from Pigeon (v3.2.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +@class FFSFileSelectorConfig; + +@interface FFSFileSelectorConfig : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithUtis:(NSArray *)utis + allowMultiSelection:(NSNumber *)allowMultiSelection; +@property(nonatomic, strong) NSArray *utis; +@property(nonatomic, strong) NSNumber *allowMultiSelection; +@end + +/// The codec used by FFSFileSelectorApi. +NSObject *FFSFileSelectorApiGetCodec(void); + +@protocol FFSFileSelectorApi +- (void)openFileSelectorWithConfig:(FFSFileSelectorConfig *)config + completion:(void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion; +@end + +extern void FFSFileSelectorApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +NS_ASSUME_NONNULL_END diff --git a/packages/file_selector/file_selector_ios/ios/Classes/messages.g.m b/packages/file_selector/file_selector_ios/ios/Classes/messages.g.m new file mode 100644 index 000000000000..d4046d281293 --- /dev/null +++ b/packages/file_selector/file_selector_ios/ios/Classes/messages.g.m @@ -0,0 +1,143 @@ +// 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.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import "messages.g.h" +#import + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSDictionary *wrapResult(id result, FlutterError *error) { + NSDictionary *errorDict = (NSDictionary *)[NSNull null]; + if (error) { + errorDict = @{ + @"code" : (error.code ?: [NSNull null]), + @"message" : (error.message ?: [NSNull null]), + @"details" : (error.details ?: [NSNull null]), + }; + } + return @{ + @"result" : (result ?: [NSNull null]), + @"error" : errorDict, + }; +} +static id GetNullableObject(NSDictionary *dict, id key) { + id result = dict[key]; + return (result == [NSNull null]) ? nil : result; +} +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + +@interface FFSFileSelectorConfig () ++ (FFSFileSelectorConfig *)fromMap:(NSDictionary *)dict; ++ (nullable FFSFileSelectorConfig *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end + +@implementation FFSFileSelectorConfig ++ (instancetype)makeWithUtis:(NSArray *)utis + allowMultiSelection:(NSNumber *)allowMultiSelection { + FFSFileSelectorConfig *pigeonResult = [[FFSFileSelectorConfig alloc] init]; + pigeonResult.utis = utis; + pigeonResult.allowMultiSelection = allowMultiSelection; + return pigeonResult; +} ++ (FFSFileSelectorConfig *)fromMap:(NSDictionary *)dict { + FFSFileSelectorConfig *pigeonResult = [[FFSFileSelectorConfig alloc] init]; + pigeonResult.utis = GetNullableObject(dict, @"utis"); + NSAssert(pigeonResult.utis != nil, @""); + pigeonResult.allowMultiSelection = GetNullableObject(dict, @"allowMultiSelection"); + NSAssert(pigeonResult.allowMultiSelection != nil, @""); + return pigeonResult; +} ++ (nullable FFSFileSelectorConfig *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FFSFileSelectorConfig fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"utis" : (self.utis ?: [NSNull null]), + @"allowMultiSelection" : (self.allowMultiSelection ?: [NSNull null]), + }; +} +@end + +@interface FFSFileSelectorApiCodecReader : FlutterStandardReader +@end +@implementation FFSFileSelectorApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FFSFileSelectorConfig fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FFSFileSelectorApiCodecWriter : FlutterStandardWriter +@end +@implementation FFSFileSelectorApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FFSFileSelectorConfig class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FFSFileSelectorApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FFSFileSelectorApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FFSFileSelectorApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FFSFileSelectorApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FFSFileSelectorApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FFSFileSelectorApiCodecReaderWriter *readerWriter = + [[FFSFileSelectorApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FFSFileSelectorApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.FileSelectorApi.openFile" + binaryMessenger:binaryMessenger + codec:FFSFileSelectorApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(openFileSelectorWithConfig:completion:)], + @"FFSFileSelectorApi api (%@) doesn't respond to " + @"@selector(openFileSelectorWithConfig:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FFSFileSelectorConfig *arg_config = GetNullableObjectAtIndex(args, 0); + [api openFileSelectorWithConfig:arg_config + completion:^(NSArray *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios.podspec b/packages/file_selector/file_selector_ios/ios/file_selector_ios.podspec new file mode 100644 index 000000000000..bb96b3c72917 --- /dev/null +++ b/packages/file_selector/file_selector_ios/ios/file_selector_ios.podspec @@ -0,0 +1,24 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint file_selector_ios.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'file_selector_ios' + s.version = '0.0.1' + s.summary = 'iOS implementation of file_selector.' + s.description = <<-DESC +Displays the native iOS document picker. + DESC + s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/file_selector' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_ios' } + s.source_files = 'Classes/**/*.{h,m}' + s.module_map = 'Classes/FileSelectorPlugin.modulemap' + s.dependency 'Flutter' + s.platform = :ios, '9.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/packages/file_selector/file_selector_ios/lib/file_selector_ios.dart b/packages/file_selector/file_selector_ios/lib/file_selector_ios.dart new file mode 100644 index 000000000000..e75f67e4f1bd --- /dev/null +++ b/packages/file_selector/file_selector_ios/lib/file_selector_ios.dart @@ -0,0 +1,64 @@ +// 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 'src/messages.g.dart'; + +/// An implementation of [FileSelectorPlatform] for iOS. +class FileSelectorIOS extends FileSelectorPlatform { + final FileSelectorApi _hostApi = FileSelectorApi(); + + /// Registers the iOS implementation. + static void registerWith() { + FileSelectorPlatform.instance = FileSelectorIOS(); + } + + @override + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final List path = (await _hostApi.openFile(FileSelectorConfig( + utis: _allowedUtiListFromTypeGroups(acceptedTypeGroups), + allowMultiSelection: false))) + .cast(); + return path.isEmpty ? null : XFile(path.first); + } + + @override + Future> openFiles({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final List pathList = (await _hostApi.openFile(FileSelectorConfig( + utis: _allowedUtiListFromTypeGroups(acceptedTypeGroups), + allowMultiSelection: true))) + .cast(); + return pathList.map((String path) => XFile(path)).toList(); + } + + // Converts the type group list into a list of all allowed UTIs, since + // iOS doesn't support filter groups. + List _allowedUtiListFromTypeGroups(List? typeGroups) { + if (typeGroups == null || typeGroups.isEmpty) { + return []; + } + final List allowedUTIs = []; + for (final XTypeGroup typeGroup in typeGroups) { + // If any group allows everything, no filtering should be done. + if (typeGroup.allowsAny) { + return []; + } + if (typeGroup.macUTIs?.isEmpty ?? true) { + throw ArgumentError('The provided type group $typeGroup should either ' + 'allow all files, or have a non-empty "macUTIs"'); + } + allowedUTIs.addAll(typeGroup.macUTIs!); + } + return allowedUTIs; + } +} diff --git a/packages/file_selector/file_selector_ios/lib/src/messages.g.dart b/packages/file_selector/file_selector_ios/lib/src/messages.g.dart new file mode 100644 index 000000000000..42184740358e --- /dev/null +++ b/packages/file_selector/file_selector_ios/lib/src/messages.g.dart @@ -0,0 +1,101 @@ +// 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.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +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 FileSelectorConfig { + FileSelectorConfig({ + required this.utis, + required this.allowMultiSelection, + }); + + List utis; + bool allowMultiSelection; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['utis'] = utis; + pigeonMap['allowMultiSelection'] = allowMultiSelection; + return pigeonMap; + } + + static FileSelectorConfig decode(Object message) { + final Map pigeonMap = message as Map; + return FileSelectorConfig( + utis: (pigeonMap['utis'] as List?)!.cast(), + allowMultiSelection: pigeonMap['allowMultiSelection']! as bool, + ); + } +} + +class _FileSelectorApiCodec extends StandardMessageCodec { + const _FileSelectorApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is FileSelectorConfig) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return FileSelectorConfig.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class FileSelectorApi { + /// Constructor for [FileSelectorApi]. 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. + FileSelectorApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _FileSelectorApiCodec(); + + Future> openFile(FileSelectorConfig arg_config) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.openFile', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_config]) 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(); + } + } +} diff --git a/packages/file_selector/file_selector_ios/pigeons/copyright.txt b/packages/file_selector/file_selector_ios/pigeons/copyright.txt new file mode 100644 index 000000000000..fb682b1ab965 --- /dev/null +++ b/packages/file_selector/file_selector_ios/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. \ No newline at end of file diff --git a/packages/file_selector/file_selector_ios/pigeons/messages.dart b/packages/file_selector/file_selector_ios/pigeons/messages.dart new file mode 100644 index 000000000000..d0ea73cde111 --- /dev/null +++ b/packages/file_selector/file_selector_ios/pigeons/messages.dart @@ -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. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + dartTestOut: 'test/test_api.dart', + objcHeaderOut: 'ios/Classes/messages.g.h', + objcSourceOut: 'ios/Classes/messages.g.m', + objcOptions: ObjcOptions( + prefix: 'FFS', + ), + copyrightHeader: 'pigeons/copyright.txt', +)) +class FileSelectorConfig { + FileSelectorConfig( + {this.utis = const [], this.allowMultiSelection = false}); + List utis; + bool allowMultiSelection; +} + +@HostApi(dartHostTestHandler: 'TestFileSelectorApi') +abstract class FileSelectorApi { + @async + @ObjCSelector('openFileSelectorWithConfig:') + List openFile(FileSelectorConfig config); +} diff --git a/packages/file_selector/file_selector_ios/pubspec.yaml b/packages/file_selector/file_selector_ios/pubspec.yaml new file mode 100644 index 000000000000..3f8ecfac04ce --- /dev/null +++ b/packages/file_selector/file_selector_ios/pubspec.yaml @@ -0,0 +1,30 @@ +name: file_selector_ios +description: iOS implementation of the file_selector plugin. +repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_ios +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 +version: 0.5.0+2 + +environment: + sdk: ">=2.14.4 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: file_selector + platforms: + ios: + dartPluginClass: FileSelectorIOS + pluginClass: FFSFileSelectorPlugin + +dependencies: + file_selector_platform_interface: ^2.2.0 + flutter: + sdk: flutter + +dev_dependencies: + build_runner: 2.1.11 + flutter_test: + sdk: flutter + mockito: ^5.1.0 + pigeon: ^3.2.5 + 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 new file mode 100644 index 000000000000..f66bd7dc7ced --- /dev/null +++ b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart @@ -0,0 +1,136 @@ +// 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_ios/file_selector_ios.dart'; +import 'package:file_selector_ios/src/messages.g.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'file_selector_ios_test.mocks.dart'; +import 'test_api.dart'; + +@GenerateMocks([TestFileSelectorApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final FileSelectorIOS plugin = FileSelectorIOS(); + late MockTestFileSelectorApi mockApi; + + setUp(() { + mockApi = MockTestFileSelectorApi(); + TestFileSelectorApi.setup(mockApi); + }); + + test('registered instance', () { + FileSelectorIOS.registerWith(); + expect(FileSelectorPlatform.instance, isA()); + }); + + group('openFile', () { + setUp(() { + when(mockApi.openFile(any)).thenAnswer((_) async => ['foo']); + }); + + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); + + await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); + + final VerificationResult result = verify(mockApi.openFile(captureAny)); + final FileSelectorConfig config = + result.captured[0] as FileSelectorConfig; + + // iOS only accepts macUTIs. + expect(listEquals(config.utis, ['public.text', 'public.image']), + isTrue); + expect(config.allowMultiSelection, isFalse); + }); + test('throws for a type group that does not support iOS', () async { + const XTypeGroup group = XTypeGroup( + label: 'images', + webWildCards: ['images/*'], + ); + + await expectLater( + plugin.openFile(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('allows a wildcard group', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + ); + + await expectLater( + plugin.openFile(acceptedTypeGroups: [group]), completes); + }); + }); + + group('openFiles', () { + setUp(() { + when(mockApi.openFile(any)).thenAnswer((_) async => ['foo']); + }); + + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); + + await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); + + final VerificationResult result = verify(mockApi.openFile(captureAny)); + final FileSelectorConfig config = + result.captured[0] as FileSelectorConfig; + + // iOS only accepts macUTIs. + expect(listEquals(config.utis, ['public.text', 'public.image']), + isTrue); + expect(config.allowMultiSelection, isTrue); + }); + test('throws for a type group that does not support iOS', () async { + const XTypeGroup group = XTypeGroup( + label: 'images', + webWildCards: ['images/*'], + ); + + await expectLater( + plugin.openFiles(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('allows a wildcard group', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + ); + + await expectLater( + plugin.openFiles(acceptedTypeGroups: [group]), completes); + }); + }); +} 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 new file mode 100644 index 000000000000..38c91b46f65e --- /dev/null +++ b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.mocks.dart @@ -0,0 +1,38 @@ +// Mocks generated by Mockito 5.3.0 from annotations +// in file_selector_ios/example/ios/.symlinks/plugins/file_selector_ios/test/file_selector_ios_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:file_selector_ios/src/messages.g.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_api.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 [TestFileSelectorApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestFileSelectorApi extends _i1.Mock + implements _i2.TestFileSelectorApi { + MockTestFileSelectorApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future> openFile(_i4.FileSelectorConfig? config) => + (super.noSuchMethod(Invocation.method(#openFile, [config]), + returnValue: _i3.Future>.value([])) + as _i3.Future>); +} diff --git a/packages/file_selector/file_selector_ios/test/test_api.dart b/packages/file_selector/file_selector_ios/test/test_api.dart new file mode 100644 index 000000000000..69f6c19d5a23 --- /dev/null +++ b/packages/file_selector/file_selector_ios/test/test_api.dart @@ -0,0 +1,70 @@ +// 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.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import +// ignore_for_file: avoid_relative_lib_imports +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'; + +// This line has been hand-edited due to +// https://github.com/flutter/flutter/issues/97744 +// ignore: directives_ordering +import 'package:file_selector_ios/src/messages.g.dart'; + +class _TestFileSelectorApiCodec extends StandardMessageCodec { + const _TestFileSelectorApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is FileSelectorConfig) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return FileSelectorConfig.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestFileSelectorApi { + static const MessageCodec codec = _TestFileSelectorApiCodec(); + + Future> openFile(FileSelectorConfig config); + static void setup(TestFileSelectorApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.openFile', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.openFile was null.'); + final List args = (message as List?)!; + final FileSelectorConfig? arg_config = + (args[0] as FileSelectorConfig?); + assert(arg_config != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.openFile was null, expected non-null FileSelectorConfig.'); + final List output = await api.openFile(arg_config!); + return {'result': output}; + }); + } + } + } +} diff --git a/packages/file_selector/file_selector_linux/.gitignore b/packages/file_selector/file_selector_linux/.gitignore new file mode 100644 index 000000000000..0393a47ff732 --- /dev/null +++ b/packages/file_selector/file_selector_linux/.gitignore @@ -0,0 +1,5 @@ +.dart_tool +.packages +.flutter-plugins +.flutter-plugins-dependencies +pubspec.lock diff --git a/packages/file_selector/file_selector_linux/AUTHORS b/packages/file_selector/file_selector_linux/AUTHORS new file mode 100644 index 000000000000..557dff97933b --- /dev/null +++ b/packages/file_selector/file_selector_linux/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/packages/file_selector/file_selector_linux/CHANGELOG.md b/packages/file_selector/file_selector_linux/CHANGELOG.md new file mode 100644 index 000000000000..a1f57b5cc857 --- /dev/null +++ b/packages/file_selector/file_selector_linux/CHANGELOG.md @@ -0,0 +1,24 @@ +## 0.9.0+1 + +* Changes XTypeGroup initialization from final to const. +* Updates minimum Flutter version to 2.10. + +## 0.9.0 + +* Moves source to flutter/plugins. + +## 0.0.3 + +* Adds Dart implementation for in-package method channel. + +## 0.0.2+1 + +* Updates README + +## 0.0.2 + +* Updates SDK constraint to signal compatibility with null safety. + +## 0.0.1 + +* Initial Linux implementation of `file_selector`. diff --git a/packages/file_selector/file_selector_linux/LICENSE b/packages/file_selector/file_selector_linux/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/file_selector/file_selector_linux/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/file_selector/file_selector_linux/README.md b/packages/file_selector/file_selector_linux/README.md new file mode 100644 index 000000000000..55a0529364b2 --- /dev/null +++ b/packages/file_selector/file_selector_linux/README.md @@ -0,0 +1,11 @@ +# file\_selector\_linux + +The Linux implementation of [`file_selector`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `file_selector` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/file_selector +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/file_selector/file_selector_linux/example/.gitignore b/packages/file_selector/file_selector_linux/example/.gitignore new file mode 100644 index 000000000000..7abd0753cfc3 --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Currently only web supported +android/ +ios/ + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/file_selector/file_selector_linux/example/.metadata b/packages/file_selector/file_selector_linux/example/.metadata new file mode 100644 index 000000000000..897381f2373f --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 7736f3bc90270dcb0480db2ccffbf1d13c28db85 + channel: dev + +project_type: app diff --git a/packages/file_selector/file_selector_linux/example/README.md b/packages/file_selector/file_selector_linux/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart b/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart new file mode 100644 index 000000000000..0699dd121541 --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart @@ -0,0 +1,83 @@ +// 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 a directory using `getDirectoryPath`, +/// then displays the selected directory in a dialog. +class GetDirectoryPage extends StatelessWidget { + /// Default Constructor + const GetDirectoryPage({Key? key}) : super(key: key); + + Future _getDirectoryPath(BuildContext context) async { + const String confirmButtonText = 'Choose'; + final String? directoryPath = + await FileSelectorPlatform.instance.getDirectoryPath( + confirmButtonText: confirmButtonText, + ); + if (directoryPath == null) { + // Operation was canceled by the user. + return; + } + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(directoryPath), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open a text file'), + ), + 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 a directory'), + onPressed: () => _getDirectoryPath(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class TextDisplay extends StatelessWidget { + /// Creates a `TextDisplay`. + const TextDisplay(this.directoryPath, {Key? key}) : super(key: key); + + /// The path selected in the dialog. + final String directoryPath; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Selected Directory'), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(directoryPath), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_linux/example/lib/home_page.dart b/packages/file_selector/file_selector_linux/example/lib/home_page.dart new file mode 100644 index 000000000000..a4b2ae1f63ea --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/lib/home_page.dart @@ -0,0 +1,63 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Home Page of the application. +class HomePage extends StatelessWidget { + /// Default Constructor + const HomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final ButtonStyle 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, + ); + return Scaffold( + appBar: AppBar( + title: const Text('File Selector Demo Home Page'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: style, + child: const Text('Open a text file'), + onPressed: () => Navigator.pushNamed(context, '/open/text'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open an image'), + onPressed: () => Navigator.pushNamed(context, '/open/image'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open multiple images'), + onPressed: () => Navigator.pushNamed(context, '/open/images'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Save a file'), + onPressed: () => Navigator.pushNamed(context, '/save/text'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open a get directory dialog'), + onPressed: () => Navigator.pushNamed(context, '/directory'), + ), + ], + ), + ), + ); + } +} diff --git a/packages/file_selector/file_selector_linux/example/lib/main.dart b/packages/file_selector/file_selector_linux/example/lib/main.dart new file mode 100644 index 000000000000..3e447104ef9f --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/lib/main.dart @@ -0,0 +1,42 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'get_directory_page.dart'; +import 'home_page.dart'; +import 'open_image_page.dart'; +import 'open_multiple_images_page.dart'; +import 'open_text_page.dart'; +import 'save_text_page.dart'; + +void main() { + runApp(const MyApp()); +} + +/// MyApp is the Main Application. +class MyApp extends StatelessWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'File Selector Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: const HomePage(), + routes: { + '/open/image': (BuildContext context) => const OpenImagePage(), + '/open/images': (BuildContext context) => + const OpenMultipleImagesPage(), + '/open/text': (BuildContext context) => const OpenTextPage(), + '/save/text': (BuildContext context) => SaveTextPage(), + '/directory': (BuildContext context) => const GetDirectoryPage(), + }, + ); + } +} 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 new file mode 100644 index 000000000000..b6ada56ebb2b --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart @@ -0,0 +1,94 @@ +// 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:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select an image file using +/// `openFiles`, then displays the selected images in a gallery dialog. +class OpenImagePage extends StatelessWidget { + /// Default Constructor + const OpenImagePage({Key? key}) : super(key: key); + + Future _openImageFile(BuildContext context) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images', + extensions: ['jpg', 'png'], + ); + final XFile? file = await FileSelectorPlatform.instance + .openFile(acceptedTypeGroups: [typeGroup]); + if (file == null) { + // Operation was canceled by the user. + return; + } + final String fileName = file.name; + final String filePath = file.path; + + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open an image'), + ), + 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 open an image file(png, jpg)'), + onPressed: () => _openImageFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays an image in a dialog. +class ImageDisplay extends StatelessWidget { + /// Default Constructor. + const ImageDisplay(this.fileName, this.filePath, {Key? key}) + : super(key: key); + + /// The name of the selected file. + final String fileName; + + /// The path to the selected file. + final String filePath; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: kIsWeb ? Image.network(filePath) : Image.file(File(filePath)), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart new file mode 100644 index 000000000000..c8e352a5b8bd --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart @@ -0,0 +1,105 @@ +// 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:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select multiple image files using +/// `openFiles`, then displays the selected images in a gallery dialog. +class OpenMultipleImagesPage extends StatelessWidget { + /// Default Constructor + const OpenMultipleImagesPage({Key? key}) : super(key: key); + + Future _openImageFile(BuildContext context) async { + const XTypeGroup jpgsTypeGroup = XTypeGroup( + label: 'JPEGs', + extensions: ['jpg', 'jpeg'], + ); + const XTypeGroup pngTypeGroup = XTypeGroup( + label: 'PNGs', + extensions: ['png'], + ); + final List files = await FileSelectorPlatform.instance + .openFiles(acceptedTypeGroups: [ + jpgsTypeGroup, + pngTypeGroup, + ]); + if (files.isEmpty) { + // Operation was canceled by the user. + return; + } + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open multiple images'), + ), + 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 open multiple images (png, jpg)'), + onPressed: () => _openImageFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class MultipleImagesDisplay extends StatelessWidget { + /// Default Constructor. + const MultipleImagesDisplay(this.files, {Key? key}) : super(key: key); + + /// The files containing the images. + final List files; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Gallery'), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: Center( + child: Row( + children: [ + ...files.map( + (XFile file) => Flexible( + child: kIsWeb + ? Image.network(file.path) + : Image.file(File(file.path))), + ) + ], + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart new file mode 100644 index 000000000000..4c88d7475049 --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart @@ -0,0 +1,91 @@ +// 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 a text file using `openFile`, then +/// displays its contents in a dialog. +class OpenTextPage extends StatelessWidget { + /// Default Constructor + const OpenTextPage({Key? key}) : super(key: key); + + Future _openTextFile(BuildContext context) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'text', + extensions: ['txt', 'json'], + ); + final XFile? file = await FileSelectorPlatform.instance + .openFile(acceptedTypeGroups: [typeGroup]); + if (file == null) { + // Operation was canceled by the user. + return; + } + final String fileName = file.name; + final String fileContent = await file.readAsString(); + + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open a text file'), + ), + 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 open a text file (json, txt)'), + onPressed: () => _openTextFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class TextDisplay extends StatelessWidget { + /// Default Constructor. + const TextDisplay(this.fileName, this.fileContent, {Key? key}) + : super(key: key); + + /// The name of the selected file. + final String fileName; + + /// The contents of the text file. + final String fileContent; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(fileContent), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart b/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart new file mode 100644 index 000000000000..9803f285a536 --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart @@ -0,0 +1,84 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select a save location using `getSavePath`, +/// then writes text to a file at that location. +class SaveTextPage extends StatelessWidget { + /// Default Constructor + SaveTextPage({Key? key}) : super(key: key); + + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _contentController = TextEditingController(); + + Future _saveFile() async { + final String fileName = _nameController.text; + final String? path = await FileSelectorPlatform.instance.getSavePath( + // Operation was canceled by the user. + suggestedName: fileName, + ); + if (path == null) { + return; + } + final String text = _contentController.text; + final Uint8List fileData = Uint8List.fromList(text.codeUnits); + const String fileMimeType = 'text/plain'; + final XFile textFile = + XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); + await textFile.saveTo(path); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Save text into a file'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 300, + child: TextField( + minLines: 1, + maxLines: 12, + controller: _nameController, + decoration: const InputDecoration( + hintText: '(Optional) Suggest File Name', + ), + ), + ), + Container( + width: 300, + child: TextField( + minLines: 1, + maxLines: 12, + controller: _contentController, + decoration: const InputDecoration( + hintText: 'Enter File Contents', + ), + ), + ), + const SizedBox(height: 10), + 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, + ), + onPressed: _saveFile, + child: const Text('Press to save a text file'), + ), + ], + ), + ), + ); + } +} diff --git a/packages/file_selector/file_selector_linux/example/linux/.gitignore b/packages/file_selector/file_selector_linux/example/linux/.gitignore new file mode 100644 index 000000000000..d3896c98444f --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/file_selector/file_selector_linux/example/linux/CMakeLists.txt b/packages/file_selector/file_selector_linux/example/linux/CMakeLists.txt new file mode 100644 index 000000000000..9d7224cc9280 --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/linux/CMakeLists.txt @@ -0,0 +1,111 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "example") +set(APPLICATION_ID "com.example.example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Application build +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +add_dependencies(${BINARY_NAME} flutter_assemble) +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Enable the test target. +set(include_file_selector_linux_tests TRUE) +# Provide an alias for the test target using the name expected by repo tooling. +add_custom_target(unit_tests DEPENDS file_selector_linux_test) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/file_selector/file_selector_linux/example/linux/flutter/CMakeLists.txt b/packages/file_selector/file_selector_linux/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000000..33fd5801e713 --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,87 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/file_selector/file_selector_linux/example/linux/flutter/generated_plugins.cmake b/packages/file_selector/file_selector_linux/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..2db3c22ae228 --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/file_selector/file_selector_linux/example/linux/main.cc b/packages/file_selector/file_selector_linux/example/linux/main.cc new file mode 100644 index 000000000000..1507d02825e7 --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/linux/main.cc @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/file_selector/file_selector_linux/example/linux/my_application.cc b/packages/file_selector/file_selector_linux/example/linux/my_application.cc new file mode 100644 index 000000000000..e970be04c827 --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/linux/my_application.cc @@ -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. + +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new( + my_application_get_type(), "application-id", APPLICATION_ID, nullptr)); +} diff --git a/packages/file_selector/file_selector_linux/example/linux/my_application.h b/packages/file_selector/file_selector_linux/example/linux/my_application.h new file mode 100644 index 000000000000..6e9f0c3ff665 --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/linux/my_application.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/file_selector/file_selector_linux/example/pubspec.yaml b/packages/file_selector/file_selector_linux/example/pubspec.yaml new file mode 100644 index 000000000000..51bdb28717aa --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/pubspec.yaml @@ -0,0 +1,21 @@ +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 +version: 1.0.0+1 + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + file_selector_linux: + path: ../ + file_selector_platform_interface: ^2.2.0 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true 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 new file mode 100644 index 000000000000..430b41c398db --- /dev/null +++ b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart @@ -0,0 +1,144 @@ +// 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/foundation.dart' show visibleForTesting; +import 'package:flutter/services.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.dev/file_selector_linux'); + +const String _typeGroupLabelKey = 'label'; +const String _typeGroupExtensionsKey = 'extensions'; +const String _typeGroupMimeTypesKey = 'mimeTypes'; + +const String _openFileMethod = 'openFile'; +const String _getSavePathMethod = 'getSavePath'; +const String _getDirectoryPathMethod = 'getDirectoryPath'; + +const String _acceptedTypeGroupsKey = 'acceptedTypeGroups'; +const String _confirmButtonTextKey = 'confirmButtonText'; +const String _initialDirectoryKey = 'initialDirectory'; +const String _multipleKey = 'multiple'; +const String _suggestedNameKey = 'suggestedName'; + +/// An implementation of [FileSelectorPlatform] for Linux. +class FileSelectorLinux extends FileSelectorPlatform { + /// The MethodChannel that is being used by this implementation of the plugin. + @visibleForTesting + MethodChannel get channel => _channel; + + /// Registers the Linux implementation. + static void registerWith() { + FileSelectorPlatform.instance = FileSelectorLinux(); + } + + @override + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final List> serializedTypeGroups = + _serializeTypeGroups(acceptedTypeGroups); + final List? path = await _channel.invokeListMethod( + _openFileMethod, + { + if (serializedTypeGroups.isNotEmpty) + _acceptedTypeGroupsKey: serializedTypeGroups, + 'initialDirectory': initialDirectory, + _confirmButtonTextKey: confirmButtonText, + _multipleKey: false, + }, + ); + return path == null ? null : XFile(path.first); + } + + @override + Future> openFiles({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final List> serializedTypeGroups = + _serializeTypeGroups(acceptedTypeGroups); + final List? pathList = await _channel.invokeListMethod( + _openFileMethod, + { + if (serializedTypeGroups.isNotEmpty) + _acceptedTypeGroupsKey: serializedTypeGroups, + _initialDirectoryKey: initialDirectory, + _confirmButtonTextKey: confirmButtonText, + _multipleKey: true, + }, + ); + return pathList?.map((String path) => XFile(path)).toList() ?? []; + } + + @override + Future getSavePath({ + List? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) async { + final List> serializedTypeGroups = + _serializeTypeGroups(acceptedTypeGroups); + return _channel.invokeMethod( + _getSavePathMethod, + { + if (serializedTypeGroups.isNotEmpty) + _acceptedTypeGroupsKey: serializedTypeGroups, + _initialDirectoryKey: initialDirectory, + _suggestedNameKey: suggestedName, + _confirmButtonTextKey: confirmButtonText, + }, + ); + } + + @override + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) async { + return _channel.invokeMethod( + _getDirectoryPathMethod, + { + _initialDirectoryKey: initialDirectory, + _confirmButtonTextKey: confirmButtonText, + }, + ); + } +} + +List> _serializeTypeGroups(List? groups) { + return (groups ?? []).map(_serializeTypeGroup).toList(); +} + +Map _serializeTypeGroup(XTypeGroup group) { + final Map serialization = { + _typeGroupLabelKey: group.label ?? '', + }; + if (group.allowsAny) { + serialization[_typeGroupExtensionsKey] = ['*']; + } else { + if ((group.extensions?.isEmpty ?? true) && + (group.mimeTypes?.isEmpty ?? true)) { + throw ArgumentError('Provided type group $group does not allow ' + 'all files, but does not set any of the Linux-supported filter ' + 'categories. "extensions" or "mimeTypes" must be non-empty for Linux ' + 'if anything is non-empty.'); + } + if (group.extensions?.isNotEmpty ?? false) { + serialization[_typeGroupExtensionsKey] = group.extensions + ?.map((String extension) => '*.$extension') + .toList() ?? + []; + } + if (group.mimeTypes?.isNotEmpty ?? false) { + serialization[_typeGroupMimeTypesKey] = group.mimeTypes ?? []; + } + } + return serialization; +} diff --git a/packages/file_selector/file_selector_linux/linux/CMakeLists.txt b/packages/file_selector/file_selector_linux/linux/CMakeLists.txt new file mode 100644 index 000000000000..d0316d94e4ac --- /dev/null +++ b/packages/file_selector/file_selector_linux/linux/CMakeLists.txt @@ -0,0 +1,67 @@ +cmake_minimum_required(VERSION 3.10) +set(PROJECT_NAME "file_selector_linux") +project(${PROJECT_NAME} LANGUAGES CXX) + +set(PLUGIN_NAME "${PROJECT_NAME}_plugin") + +list(APPEND PLUGIN_SOURCES + "file_selector_plugin.cc" +) + +add_library(${PLUGIN_NAME} SHARED + "file_selector_plugin.cc" +) +apply_standard_settings(${PLUGIN_NAME}) +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) +target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) + + +# === Tests === + +if(${include_${PROJECT_NAME}_tests}) +if(${CMAKE_VERSION} VERSION_LESS "3.11.0") +message("Unit tests require CMake 3.11.0 or later") +else() +set(TEST_RUNNER "${PROJECT_NAME}_test") +enable_testing() +# TODO(stuartmorgan): Consider using a single shared, pre-checked-in googletest +# instance rather than downloading for each plugin. This approach makes sense +# for a template, but not for a monorepo with many plugins. +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/release-1.11.0.zip +) +# Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# Disable install commands for gtest so it doesn't end up in the bundle. +set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) + +FetchContent_MakeAvailable(googletest) + +# The plugin's exported API is not very useful for unit testing, so build the +# sources directly into the test binary rather than using the shared library. +add_executable(${TEST_RUNNER} + test/file_selector_plugin_test.cc + test/test_main.cc + ${PLUGIN_SOURCES} +) +apply_standard_settings(${TEST_RUNNER}) +target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(${TEST_RUNNER} PRIVATE flutter) +target_link_libraries(${TEST_RUNNER} PRIVATE PkgConfig::GTK) +target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) + +include(GoogleTest) +# TODO(stuartmorgan): Switch back to gtest_discover_tests when moving to +# flutter/plugins; it doesn't work in the FDE CI because it requires actually +# running a GTK app, which it hasn't been set up for. +gtest_add_tests(TARGET ${TEST_RUNNER}) +#gtest_discover_tests(${TEST_RUNNER}) +endif() # CMake version check +endif() # include_${PROJECT_NAME}_tests 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 new file mode 100644 index 000000000000..833771955120 --- /dev/null +++ b/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc @@ -0,0 +1,246 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "include/file_selector_linux/file_selector_plugin.h" + +#include +#include + +#include "file_selector_plugin_private.h" + +// From file_selector_linux.dart +const char kChannelName[] = "plugins.flutter.dev/file_selector_linux"; + +const char kOpenFileMethod[] = "openFile"; +const char kGetSavePathMethod[] = "getSavePath"; +const char kGetDirectoryPathMethod[] = "getDirectoryPath"; + +const char kAcceptedTypeGroupsKey[] = "acceptedTypeGroups"; +const char kConfirmButtonTextKey[] = "confirmButtonText"; +const char kInitialDirectoryKey[] = "initialDirectory"; +const char kMultipleKey[] = "multiple"; +const char kSuggestedNameKey[] = "suggestedName"; + +const char kTypeGroupLabelKey[] = "label"; +const char kTypeGroupExtensionsKey[] = "extensions"; +const char kTypeGroupMimeTypesKey[] = "mimeTypes"; + +// Errors +const char kBadArgumentsError[] = "Bad Arguments"; +const char kNoScreenError[] = "No Screen"; + +struct _FlFileSelectorPlugin { + GObject parent_instance; + + FlPluginRegistrar* registrar; + + // Connection to Flutter engine. + FlMethodChannel* channel; +}; + +G_DEFINE_TYPE(FlFileSelectorPlugin, fl_file_selector_plugin, G_TYPE_OBJECT) + +// Converts a type group received from Flutter into a GTK file filter. +static GtkFileFilter* type_group_to_filter(FlValue* value) { + g_autoptr(GtkFileFilter) filter = gtk_file_filter_new(); + + FlValue* label = fl_value_lookup_string(value, kTypeGroupLabelKey); + if (label != nullptr && fl_value_get_type(label) == FL_VALUE_TYPE_STRING) { + gtk_file_filter_set_name(filter, fl_value_get_string(label)); + } + + FlValue* extensions = fl_value_lookup_string(value, kTypeGroupExtensionsKey); + if (extensions != nullptr && + fl_value_get_type(extensions) == FL_VALUE_TYPE_LIST) { + for (size_t i = 0; i < fl_value_get_length(extensions); i++) { + FlValue* v = fl_value_get_list_value(extensions, i); + const gchar* pattern = fl_value_get_string(v); + gtk_file_filter_add_pattern(filter, pattern); + } + } + FlValue* mime_types = fl_value_lookup_string(value, kTypeGroupMimeTypesKey); + if (mime_types != nullptr && + fl_value_get_type(mime_types) == FL_VALUE_TYPE_LIST) { + for (size_t i = 0; i < fl_value_get_length(mime_types); i++) { + FlValue* v = fl_value_get_list_value(mime_types, i); + const gchar* pattern = fl_value_get_string(v); + gtk_file_filter_add_mime_type(filter, pattern); + } + } + + return GTK_FILE_FILTER(g_object_ref(filter)); +} + +// Creates a GtkFileChooserNative for the given method call details. +static GtkFileChooserNative* create_dialog( + GtkWindow* window, GtkFileChooserAction action, const gchar* title, + const gchar* default_confirm_button_text, FlValue* properties) { + const gchar* confirm_button_text = default_confirm_button_text; + FlValue* value = fl_value_lookup_string(properties, kConfirmButtonTextKey); + if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_STRING) + confirm_button_text = fl_value_get_string(value); + + g_autoptr(GtkFileChooserNative) dialog = + GTK_FILE_CHOOSER_NATIVE(gtk_file_chooser_native_new( + title, window, action, confirm_button_text, "_Cancel")); + + value = fl_value_lookup_string(properties, kMultipleKey); + if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_BOOL) { + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), + fl_value_get_bool(value)); + } + + value = fl_value_lookup_string(properties, kInitialDirectoryKey); + if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_STRING) { + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), + fl_value_get_string(value)); + } + + value = fl_value_lookup_string(properties, kSuggestedNameKey); + if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_STRING) { + gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), + fl_value_get_string(value)); + } + + value = fl_value_lookup_string(properties, kAcceptedTypeGroupsKey); + if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_LIST) { + for (size_t i = 0; i < fl_value_get_length(value); i++) { + FlValue* type_group = fl_value_get_list_value(value, i); + GtkFileFilter* filter = type_group_to_filter(type_group); + if (filter == nullptr) { + return nullptr; + } + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter); + } + } + + return GTK_FILE_CHOOSER_NATIVE(g_object_ref(dialog)); +} + +// TODO(stuartmorgan): Move this logic back into method_call_cb once +// https://github.com/flutter/flutter/issues/88724 is fixed, and test +// through the public API instead. This only exists to move as much +// logic as possible behind the private entry point used by unit tests. +GtkFileChooserNative* create_dialog_for_method(GtkWindow* window, + const gchar* method, + FlValue* properties) { + if (strcmp(method, kOpenFileMethod) == 0) { + return create_dialog(window, GTK_FILE_CHOOSER_ACTION_OPEN, "Open File", + "_Open", properties); + } else if (strcmp(method, kGetDirectoryPathMethod) == 0) { + return create_dialog(window, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, + "Choose Directory", "_Open", properties); + } else if (strcmp(method, kGetSavePathMethod) == 0) { + return create_dialog(window, GTK_FILE_CHOOSER_ACTION_SAVE, "Save File", + "_Save", properties); + } + return nullptr; +} + +// Shows the requested dialog type. +static FlMethodResponse* show_dialog(FlFileSelectorPlugin* self, + const gchar* method, FlValue* properties, + bool return_list) { + if (fl_value_get_type(properties) != FL_VALUE_TYPE_MAP) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Argument map missing or malformed", nullptr)); + } + + FlView* view = fl_plugin_registrar_get_view(self->registrar); + if (view == nullptr) { + return FL_METHOD_RESPONSE( + fl_method_error_response_new(kNoScreenError, nullptr, nullptr)); + } + GtkWindow* window = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(view))); + + g_autoptr(GtkFileChooserNative) dialog = + create_dialog_for_method(window, method, properties); + + if (dialog == nullptr) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, "Unable to create dialog from arguments", nullptr)); + } + + gint response = gtk_native_dialog_run(GTK_NATIVE_DIALOG(dialog)); + g_autoptr(FlValue) result = nullptr; + if (response == GTK_RESPONSE_ACCEPT) { + if (return_list) { + result = fl_value_new_list(); + g_autoptr(GSList) filenames = + gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog)); + for (GSList* link = filenames; link != nullptr; link = link->next) { + g_autofree gchar* filename = static_cast(link->data); + fl_value_append_take(result, fl_value_new_string(filename)); + } + } else { + g_autofree gchar* filename = + gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); + result = fl_value_new_string(filename); + } + } + + return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); +} + +// Called when a method call is received from Flutter. +static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call, + gpointer user_data) { + FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN(user_data); + + const gchar* method = fl_method_call_get_name(method_call); + FlValue* args = fl_method_call_get_args(method_call); + + g_autoptr(FlMethodResponse) response = nullptr; + if (strcmp(method, kOpenFileMethod) == 0) { + response = show_dialog(self, method, args, true); + } else if (strcmp(method, kGetDirectoryPathMethod) == 0 || + strcmp(method, kGetSavePathMethod) == 0) { + response = show_dialog(self, method, args, false); + } else { + response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); + } + + g_autoptr(GError) error = nullptr; + if (!fl_method_call_respond(method_call, response, &error)) + g_warning("Failed to send method call response: %s", error->message); +} + +static void fl_file_selector_plugin_dispose(GObject* object) { + FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN(object); + + g_clear_object(&self->registrar); + g_clear_object(&self->channel); + + G_OBJECT_CLASS(fl_file_selector_plugin_parent_class)->dispose(object); +} + +static void fl_file_selector_plugin_class_init( + FlFileSelectorPluginClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_file_selector_plugin_dispose; +} + +static void fl_file_selector_plugin_init(FlFileSelectorPlugin* self) {} + +FlFileSelectorPlugin* fl_file_selector_plugin_new( + FlPluginRegistrar* registrar) { + FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN( + g_object_new(fl_file_selector_plugin_get_type(), nullptr)); + + self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar)); + + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + self->channel = + fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar), + kChannelName, FL_METHOD_CODEC(codec)); + fl_method_channel_set_method_call_handler(self->channel, method_call_cb, + g_object_ref(self), g_object_unref); + + return self; +} + +void file_selector_plugin_register_with_registrar( + FlPluginRegistrar* registrar) { + FlFileSelectorPlugin* plugin = fl_file_selector_plugin_new(registrar); + g_object_unref(plugin); +} diff --git a/packages/file_selector/file_selector_linux/linux/file_selector_plugin_private.h b/packages/file_selector/file_selector_linux/linux/file_selector_plugin_private.h new file mode 100644 index 000000000000..e58a78ccda37 --- /dev/null +++ b/packages/file_selector/file_selector_linux/linux/file_selector_plugin_private.h @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "include/file_selector_linux/file_selector_plugin.h" + +// Creates a GtkFileChooserNative for the given method call. +GtkFileChooserNative* create_dialog_for_method(GtkWindow* window, + const gchar* method, + FlValue* properties); diff --git a/packages/file_selector/file_selector_linux/linux/include/file_selector_linux/file_selector_plugin.h b/packages/file_selector/file_selector_linux/linux/include/file_selector_linux/file_selector_plugin.h new file mode 100644 index 000000000000..98e90e5d68ab --- /dev/null +++ b/packages/file_selector/file_selector_linux/linux/include/file_selector_linux/file_selector_plugin.h @@ -0,0 +1,31 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLUGINS_FILE_SELECTOR_LINUX_FILE_SELECTOR_PLUGIN_H_ +#define PLUGINS_FILE_SELECTOR_LINUX_FILE_SELECTOR_PLUGIN_H_ + +// A plugin to show native save/open file choosers. + +#include + +G_BEGIN_DECLS + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +G_DECLARE_FINAL_TYPE(FlFileSelectorPlugin, fl_file_selector_plugin, FL, + FILE_SELECTOR_PLUGIN, GObject) + +FLUTTER_PLUGIN_EXPORT FlFileSelectorPlugin* fl_file_selector_plugin_new( + FlPluginRegistrar* registrar); + +FLUTTER_PLUGIN_EXPORT void file_selector_plugin_register_with_registrar( + FlPluginRegistrar* registrar); + +G_END_DECLS + +#endif // PLUGINS_FILE_SELECTOR_LINUX_FILE_SELECTOR_PLUGIN_H_ 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 new file mode 100644 index 000000000000..84c55ac91900 --- /dev/null +++ b/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc @@ -0,0 +1,171 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "include/file_selector_linux/file_selector_plugin.h" + +#include +#include +#include + +#include "file_selector_plugin_private.h" + +// TODO(stuartmorgan): Restructure the helper to take a callback for showing +// the dialog, so that the tests can mock out that callback with something +// that changes the selection so that the return value path can be tested +// as well. +// TODO(stuartmorgan): Add an injectable wrapper around +// gtk_file_chooser_native_new to allow for testing values that are given as +// construction paramaters and can't be queried later. + +TEST(FileSelectorPlugin, TestOpenSimple) { + g_autoptr(FlValue) args = fl_value_new_map(); + + g_autoptr(GtkFileChooserNative) dialog = + create_dialog_for_method(nullptr, "openFile", args); + + ASSERT_NE(dialog, nullptr); + EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), + GTK_FILE_CHOOSER_ACTION_OPEN); + EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)), + false); +} + +TEST(FileSelectorPlugin, TestOpenMultiple) { + 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, "openFile", args); + + ASSERT_NE(dialog, nullptr); + EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), + GTK_FILE_CHOOSER_ACTION_OPEN); + EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)), + true); +} + +TEST(FileSelectorPlugin, TestOpenWithFilter) { + g_autoptr(FlValue) type_groups = fl_value_new_list(); + + { + g_autoptr(FlValue) text_group_mime_types = fl_value_new_list(); + fl_value_append_take(text_group_mime_types, + fl_value_new_string("text/plain")); + g_autoptr(FlValue) text_group = fl_value_new_map(); + fl_value_set_string_take(text_group, "label", fl_value_new_string("Text")); + fl_value_set_string(text_group, "mimeTypes", text_group_mime_types); + fl_value_append(type_groups, text_group); + } + + { + g_autoptr(FlValue) image_group_extensions = fl_value_new_list(); + fl_value_append_take(image_group_extensions, fl_value_new_string("*.png")); + fl_value_append_take(image_group_extensions, fl_value_new_string("*.gif")); + fl_value_append_take(image_group_extensions, + fl_value_new_string("*.jgpeg")); + g_autoptr(FlValue) image_group = fl_value_new_map(); + fl_value_set_string_take(image_group, "label", + fl_value_new_string("Images")); + fl_value_set_string(image_group, "extensions", image_group_extensions); + fl_value_append(type_groups, image_group); + } + + { + g_autoptr(FlValue) any_group_extensions = fl_value_new_list(); + fl_value_append_take(any_group_extensions, fl_value_new_string("*")); + g_autoptr(FlValue) any_group = fl_value_new_map(); + fl_value_set_string_take(any_group, "label", fl_value_new_string("Any")); + fl_value_set_string(any_group, "extensions", any_group_extensions); + fl_value_append(type_groups, any_group); + } + + g_autoptr(FlValue) args = fl_value_new_map(); + fl_value_set_string(args, "acceptedTypeGroups", type_groups); + + g_autoptr(GtkFileChooserNative) dialog = + create_dialog_for_method(nullptr, "openFile", args); + + ASSERT_NE(dialog, nullptr); + EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), + GTK_FILE_CHOOSER_ACTION_OPEN); + EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)), + false); + // Validate filters. + g_autoptr(GSList) type_group_list = + gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(dialog)); + EXPECT_EQ(g_slist_length(type_group_list), 3); + GtkFileFilter* text_filter = + GTK_FILE_FILTER(g_slist_nth_data(type_group_list, 0)); + GtkFileFilter* image_filter = + GTK_FILE_FILTER(g_slist_nth_data(type_group_list, 1)); + GtkFileFilter* any_filter = + GTK_FILE_FILTER(g_slist_nth_data(type_group_list, 2)); + // Filters can't be inspected, so query them to see that they match expected + // filter behavior. + GtkFileFilterInfo text_file_info = {}; + text_file_info.contains = static_cast( + GTK_FILE_FILTER_DISPLAY_NAME | GTK_FILE_FILTER_MIME_TYPE); + text_file_info.display_name = "foo.txt"; + text_file_info.mime_type = "text/plain"; + GtkFileFilterInfo image_file_info = {}; + image_file_info.contains = static_cast( + GTK_FILE_FILTER_DISPLAY_NAME | GTK_FILE_FILTER_MIME_TYPE); + image_file_info.display_name = "foo.png"; + image_file_info.mime_type = "image/png"; + EXPECT_TRUE(gtk_file_filter_filter(text_filter, &text_file_info)); + EXPECT_FALSE(gtk_file_filter_filter(text_filter, &image_file_info)); + EXPECT_FALSE(gtk_file_filter_filter(image_filter, &text_file_info)); + EXPECT_TRUE(gtk_file_filter_filter(image_filter, &image_file_info)); + EXPECT_TRUE(gtk_file_filter_filter(any_filter, &image_file_info)); + EXPECT_TRUE(gtk_file_filter_filter(any_filter, &text_file_info)); +} + +TEST(FileSelectorPlugin, TestSaveSimple) { + g_autoptr(FlValue) args = fl_value_new_map(); + + g_autoptr(GtkFileChooserNative) dialog = + create_dialog_for_method(nullptr, "getSavePath", args); + + ASSERT_NE(dialog, nullptr); + EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), + GTK_FILE_CHOOSER_ACTION_SAVE); + EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)), + false); +} + +TEST(FileSelectorPlugin, TestSaveWithArguments) { + g_autoptr(FlValue) args = fl_value_new_map(); + fl_value_set_string_take(args, "initialDirectory", + fl_value_new_string("/tmp")); + fl_value_set_string_take(args, "suggestedName", + fl_value_new_string("foo.txt")); + + g_autoptr(GtkFileChooserNative) dialog = + create_dialog_for_method(nullptr, "getSavePath", args); + + ASSERT_NE(dialog, nullptr); + EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), + GTK_FILE_CHOOSER_ACTION_SAVE); + EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)), + false); + g_autofree gchar* current_name = + gtk_file_chooser_get_current_name(GTK_FILE_CHOOSER(dialog)); + EXPECT_STREQ(current_name, "foo.txt"); + // TODO(stuartmorgan): gtk_file_chooser_get_current_folder doesn't seem to + // return a value set by gtk_file_chooser_set_current_folder, or at least + // doesn't in a test context, so that's not currently validated. +} + +TEST(FileSelectorPlugin, TestGetDirectory) { + g_autoptr(FlValue) args = fl_value_new_map(); + + 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)), + false); +} diff --git a/packages/file_selector/file_selector_linux/linux/test/test_main.cc b/packages/file_selector/file_selector_linux/linux/test/test_main.cc new file mode 100644 index 000000000000..7e33b21fd419 --- /dev/null +++ b/packages/file_selector/file_selector_linux/linux/test/test_main.cc @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include + +int main(int argc, char** argv) { + gtk_init(0, nullptr); + + testing::InitGoogleTest(&argc, argv); + int exit_code = RUN_ALL_TESTS(); + + return exit_code; +} diff --git a/packages/file_selector/file_selector_linux/pubspec.yaml b/packages/file_selector/file_selector_linux/pubspec.yaml new file mode 100644 index 000000000000..a8aea37d72e2 --- /dev/null +++ b/packages/file_selector/file_selector_linux/pubspec.yaml @@ -0,0 +1,27 @@ +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 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: file_selector + platforms: + linux: + pluginClass: FileSelectorPlugin + dartPluginClass: FileSelectorLinux + +dependencies: + cross_file: ^0.3.1 + file_selector_platform_interface: ^2.2.0 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + 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 new file mode 100644 index 000000000000..748f922ae6ef --- /dev/null +++ b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart @@ -0,0 +1,389 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_linux/file_selector_linux.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late FileSelectorLinux plugin; + late List log; + + setUp(() { + plugin = FileSelectorLinux(); + log = []; + plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return null; + }); + }); + + test('registers instance', () { + FileSelectorLinux.registerWith(); + expect(FileSelectorPlatform.instance, isA()); + }); + + group('#openFile', () { + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*'], + ); + + await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); + + expect( + 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, + }), + ], + ); + }); + + test('passes initialDirectory correctly', () async { + await plugin.openFile(initialDirectory: '/example/directory'); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': false, + }), + ], + ); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.openFile(confirmButtonText: 'Open File'); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': false, + }), + ], + ); + }); + + test('throws for a type group that does not support Linux', () async { + const XTypeGroup group = XTypeGroup( + label: 'images', + webWildCards: ['images/*'], + ); + + await expectLater( + plugin.openFile(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('passes a wildcard group correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'any', + ); + + await plugin.openFile(acceptedTypeGroups: [group]); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'any', + 'extensions': ['*'], + }, + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }), + ], + ); + }); + }); + + group('#openFiles', () { + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*'], + ); + + await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); + + expect( + 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, + }), + ], + ); + }); + + test('passes initialDirectory correctly', () async { + await plugin.openFiles(initialDirectory: '/example/directory'); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': true, + }), + ], + ); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.openFiles(confirmButtonText: 'Open File'); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': true, + }), + ], + ); + }); + + test('throws for a type group that does not support Linux', () async { + const XTypeGroup group = XTypeGroup( + label: 'images', + webWildCards: ['images/*'], + ); + + await expectLater( + plugin.openFile(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('passes a wildcard group correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'any', + ); + + await plugin.openFile(acceptedTypeGroups: [group]); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'any', + 'extensions': ['*'], + }, + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }), + ], + ); + }); + }); + + group('#getSavePath', () { + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*'], + ); + + await plugin + .getSavePath(acceptedTypeGroups: [group, groupTwo]); + + expect( + 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, + }), + ], + ); + }); + + test('passes initialDirectory correctly', () async { + await plugin.getSavePath(initialDirectory: '/example/directory'); + + expect( + log, + [ + isMethodCall('getSavePath', arguments: { + 'initialDirectory': '/example/directory', + 'suggestedName': null, + 'confirmButtonText': null, + }), + ], + ); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.getSavePath(confirmButtonText: 'Open File'); + + expect( + log, + [ + isMethodCall('getSavePath', arguments: { + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': 'Open File', + }), + ], + ); + }); + + test('throws for a type group that does not support Linux', () async { + const XTypeGroup group = XTypeGroup( + label: 'images', + webWildCards: ['images/*'], + ); + + await expectLater( + plugin.openFile(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('passes a wildcard group correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'any', + ); + + await plugin.openFile(acceptedTypeGroups: [group]); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'any', + 'extensions': ['*'], + }, + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }), + ], + ); + }); + }); + + group('#getDirectoryPath', () { + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPath(initialDirectory: '/example/directory'); + + expect( + log, + [ + isMethodCall('getDirectoryPath', arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + }), + ], + ); + }); + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPath(confirmButtonText: 'Open File'); + + expect( + log, + [ + isMethodCall('getDirectoryPath', arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + }), + ], + ); + }); + }); +} diff --git a/packages/file_selector/file_selector_macos/.gitignore b/packages/file_selector/file_selector_macos/.gitignore new file mode 100644 index 000000000000..0393a47ff732 --- /dev/null +++ b/packages/file_selector/file_selector_macos/.gitignore @@ -0,0 +1,5 @@ +.dart_tool +.packages +.flutter-plugins +.flutter-plugins-dependencies +pubspec.lock diff --git a/packages/file_selector/file_selector_macos/.metadata b/packages/file_selector/file_selector_macos/.metadata new file mode 100644 index 000000000000..720a4596c087 --- /dev/null +++ b/packages/file_selector/file_selector_macos/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 6d1c244b79f3a2747281f718297ce248bd5ad099 + channel: master + +project_type: plugin diff --git a/packages/file_selector/file_selector_macos/AUTHORS b/packages/file_selector/file_selector_macos/AUTHORS new file mode 100644 index 000000000000..557dff97933b --- /dev/null +++ b/packages/file_selector/file_selector_macos/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md new file mode 100644 index 000000000000..af17db8ae3ef --- /dev/null +++ b/packages/file_selector/file_selector_macos/CHANGELOG.md @@ -0,0 +1,57 @@ +## 0.9.0+3 + +* Changes XTypeGroup initialization from final to const. + +## 0.9.0+2 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. + +## 0.9.0+1 + +* Updates README for endorsement. +* Updates `flutter_test` to be a `dev_dependencies` entry. + +## 0.9.0 + +* **BREAKING CHANGE**: Methods that take `XTypeGroup`s now throw an + `ArgumentError` if any group is not a wildcard (all filter types null or + empty), but doesn't include any of the filter types supported by macOS. +* Ignores deprecation warnings for upcoming styleFrom button API changes. + +## 0.8.2+2 + +* Updates references to the obsolete master branch. + +## 0.8.2+1 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.8.2 + +* Moves source to flutter/plugins. +* Adds native unit tests. +* Converts native implementation to Swift. +* Switches to an internal method channel implementation. + +## 0.0.4+1 + +* Update README + +## 0.0.4 + +* Treat empty filter lists the same as null. + +## 0.0.3 + +* Fix README + +## 0.0.2 + +* Update SDK constraint to signal compatibility with null safety. + +## 0.0.1 + +* Initial macOS implementation of `file_selector`. diff --git a/packages/file_selector/file_selector_macos/LICENSE b/packages/file_selector/file_selector_macos/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/file_selector/file_selector_macos/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/file_selector/file_selector_macos/README.md b/packages/file_selector/file_selector_macos/README.md new file mode 100644 index 000000000000..10a5636ef4b1 --- /dev/null +++ b/packages/file_selector/file_selector_macos/README.md @@ -0,0 +1,26 @@ +# file\_selector\_macos + +The macOS implementation of [`file_selector`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `file_selector` +normally. This package will be automatically included in your app when you do. + +### Entitlements + +You will need to [add an entitlement][3] for either read-only access: +```xml + com.apple.security.files.user-selected.read-only + +``` +or read/write access: +```xml + com.apple.security.files.user-selected.read-write + +``` +depending on your use case. + +[1]: https://pub.dev/packages/file_selector +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[3]: https://flutter.dev/desktop#entitlements-and-the-app-sandbox diff --git a/packages/file_selector/file_selector_macos/example/.gitignore b/packages/file_selector/file_selector_macos/example/.gitignore new file mode 100644 index 000000000000..7abd0753cfc3 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Currently only web supported +android/ +ios/ + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/file_selector/file_selector_macos/example/.metadata b/packages/file_selector/file_selector_macos/example/.metadata new file mode 100644 index 000000000000..897381f2373f --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 7736f3bc90270dcb0480db2ccffbf1d13c28db85 + channel: dev + +project_type: app diff --git a/packages/file_selector/file_selector_macos/example/README.md b/packages/file_selector/file_selector_macos/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/file_selector/file_selector_macos/example/lib/get_directory_page.dart b/packages/file_selector/file_selector_macos/example/lib/get_directory_page.dart new file mode 100644 index 000000000000..a2a209dc9529 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/lib/get_directory_page.dart @@ -0,0 +1,83 @@ +// 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 a directory using `getDirectoryPath`, +/// then displays the selected directory in a dialog. +class GetDirectoryPage extends StatelessWidget { + /// Default Constructor + const GetDirectoryPage({Key? key}) : super(key: key); + + Future _getDirectoryPath(BuildContext context) async { + const String confirmButtonText = 'Choose'; + final String? directoryPath = + await FileSelectorPlatform.instance.getDirectoryPath( + confirmButtonText: confirmButtonText, + ); + if (directoryPath == null) { + // Operation was canceled by the user. + return; + } + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(directoryPath), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open a text file'), + ), + 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 a directory'), + onPressed: () => _getDirectoryPath(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class TextDisplay extends StatelessWidget { + /// Creates a `TextDisplay`. + const TextDisplay(this.directoryPath, {Key? key}) : super(key: key); + + /// The path selected in the dialog. + final String directoryPath; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Selected Directory'), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(directoryPath), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_macos/example/lib/home_page.dart b/packages/file_selector/file_selector_macos/example/lib/home_page.dart new file mode 100644 index 000000000000..a4b2ae1f63ea --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/lib/home_page.dart @@ -0,0 +1,63 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Home Page of the application. +class HomePage extends StatelessWidget { + /// Default Constructor + const HomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final ButtonStyle 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, + ); + return Scaffold( + appBar: AppBar( + title: const Text('File Selector Demo Home Page'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: style, + child: const Text('Open a text file'), + onPressed: () => Navigator.pushNamed(context, '/open/text'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open an image'), + onPressed: () => Navigator.pushNamed(context, '/open/image'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open multiple images'), + onPressed: () => Navigator.pushNamed(context, '/open/images'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Save a file'), + onPressed: () => Navigator.pushNamed(context, '/save/text'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open a get directory dialog'), + onPressed: () => Navigator.pushNamed(context, '/directory'), + ), + ], + ), + ), + ); + } +} diff --git a/packages/file_selector/file_selector_macos/example/lib/main.dart b/packages/file_selector/file_selector_macos/example/lib/main.dart new file mode 100644 index 000000000000..3e447104ef9f --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/lib/main.dart @@ -0,0 +1,42 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'get_directory_page.dart'; +import 'home_page.dart'; +import 'open_image_page.dart'; +import 'open_multiple_images_page.dart'; +import 'open_text_page.dart'; +import 'save_text_page.dart'; + +void main() { + runApp(const MyApp()); +} + +/// MyApp is the Main Application. +class MyApp extends StatelessWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'File Selector Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: const HomePage(), + routes: { + '/open/image': (BuildContext context) => const OpenImagePage(), + '/open/images': (BuildContext context) => + const OpenMultipleImagesPage(), + '/open/text': (BuildContext context) => const OpenTextPage(), + '/save/text': (BuildContext context) => SaveTextPage(), + '/directory': (BuildContext context) => const GetDirectoryPage(), + }, + ); + } +} 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 new file mode 100644 index 000000000000..b6ada56ebb2b --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/lib/open_image_page.dart @@ -0,0 +1,94 @@ +// 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:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select an image file using +/// `openFiles`, then displays the selected images in a gallery dialog. +class OpenImagePage extends StatelessWidget { + /// Default Constructor + const OpenImagePage({Key? key}) : super(key: key); + + Future _openImageFile(BuildContext context) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images', + extensions: ['jpg', 'png'], + ); + final XFile? file = await FileSelectorPlatform.instance + .openFile(acceptedTypeGroups: [typeGroup]); + if (file == null) { + // Operation was canceled by the user. + return; + } + final String fileName = file.name; + final String filePath = file.path; + + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open an image'), + ), + 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 open an image file(png, jpg)'), + onPressed: () => _openImageFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays an image in a dialog. +class ImageDisplay extends StatelessWidget { + /// Default Constructor. + const ImageDisplay(this.fileName, this.filePath, {Key? key}) + : super(key: key); + + /// The name of the selected file. + final String fileName; + + /// The path to the selected file. + final String filePath; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: kIsWeb ? Image.network(filePath) : Image.file(File(filePath)), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_macos/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_macos/example/lib/open_multiple_images_page.dart new file mode 100644 index 000000000000..c8e352a5b8bd --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/lib/open_multiple_images_page.dart @@ -0,0 +1,105 @@ +// 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:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select multiple image files using +/// `openFiles`, then displays the selected images in a gallery dialog. +class OpenMultipleImagesPage extends StatelessWidget { + /// Default Constructor + const OpenMultipleImagesPage({Key? key}) : super(key: key); + + Future _openImageFile(BuildContext context) async { + const XTypeGroup jpgsTypeGroup = XTypeGroup( + label: 'JPEGs', + extensions: ['jpg', 'jpeg'], + ); + const XTypeGroup pngTypeGroup = XTypeGroup( + label: 'PNGs', + extensions: ['png'], + ); + final List files = await FileSelectorPlatform.instance + .openFiles(acceptedTypeGroups: [ + jpgsTypeGroup, + pngTypeGroup, + ]); + if (files.isEmpty) { + // Operation was canceled by the user. + return; + } + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open multiple images'), + ), + 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 open multiple images (png, jpg)'), + onPressed: () => _openImageFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class MultipleImagesDisplay extends StatelessWidget { + /// Default Constructor. + const MultipleImagesDisplay(this.files, {Key? key}) : super(key: key); + + /// The files containing the images. + final List files; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Gallery'), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: Center( + child: Row( + children: [ + ...files.map( + (XFile file) => Flexible( + child: kIsWeb + ? Image.network(file.path) + : Image.file(File(file.path))), + ) + ], + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_macos/example/lib/open_text_page.dart b/packages/file_selector/file_selector_macos/example/lib/open_text_page.dart new file mode 100644 index 000000000000..4c88d7475049 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/lib/open_text_page.dart @@ -0,0 +1,91 @@ +// 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 a text file using `openFile`, then +/// displays its contents in a dialog. +class OpenTextPage extends StatelessWidget { + /// Default Constructor + const OpenTextPage({Key? key}) : super(key: key); + + Future _openTextFile(BuildContext context) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'text', + extensions: ['txt', 'json'], + ); + final XFile? file = await FileSelectorPlatform.instance + .openFile(acceptedTypeGroups: [typeGroup]); + if (file == null) { + // Operation was canceled by the user. + return; + } + final String fileName = file.name; + final String fileContent = await file.readAsString(); + + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open a text file'), + ), + 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 open a text file (json, txt)'), + onPressed: () => _openTextFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class TextDisplay extends StatelessWidget { + /// Default Constructor. + const TextDisplay(this.fileName, this.fileContent, {Key? key}) + : super(key: key); + + /// The name of the selected file. + final String fileName; + + /// The contents of the text file. + final String fileContent; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(fileContent), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart b/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart new file mode 100644 index 000000000000..3f215fea0a23 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart @@ -0,0 +1,84 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select a save location using `getSavePath`, +/// then writes text to a file at that location. +class SaveTextPage extends StatelessWidget { + /// Default Constructor + SaveTextPage({Key? key}) : super(key: key); + + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _contentController = TextEditingController(); + + Future _saveFile() async { + final String fileName = _nameController.text; + final String? path = await FileSelectorPlatform.instance.getSavePath( + suggestedName: fileName, + ); + if (path == null) { + // Operation was canceled by the user. + return; + } + final String text = _contentController.text; + final Uint8List fileData = Uint8List.fromList(text.codeUnits); + const String fileMimeType = 'text/plain'; + final XFile textFile = + XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); + await textFile.saveTo(path); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Save text into a file'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 300, + child: TextField( + minLines: 1, + maxLines: 12, + controller: _nameController, + decoration: const InputDecoration( + hintText: '(Optional) Suggest File Name', + ), + ), + ), + Container( + width: 300, + child: TextField( + minLines: 1, + maxLines: 12, + controller: _contentController, + decoration: const InputDecoration( + hintText: 'Enter File Contents', + ), + ), + ), + const SizedBox(height: 10), + 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, + ), + onPressed: _saveFile, + child: const Text('Press to save a text file'), + ), + ], + ), + ), + ); + } +} diff --git a/packages/file_selector/file_selector_macos/example/macos/.gitignore b/packages/file_selector/file_selector_macos/example/macos/.gitignore new file mode 100644 index 000000000000..d2fd3772308c --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/.gitignore @@ -0,0 +1,6 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/xcuserdata/ diff --git a/packages/file_selector/file_selector_macos/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/file_selector/file_selector_macos/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000000..4b81f9b2d200 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/file_selector/file_selector_macos/example/macos/Flutter/Flutter-Release.xcconfig b/packages/file_selector/file_selector_macos/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000000..5caa9d1579e4 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/file_selector/file_selector_macos/example/macos/Podfile b/packages/file_selector/file_selector_macos/example/macos/Podfile new file mode 100644 index 000000000000..dade8dfad0dc --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..fa8d272d4ee0 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,767 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 338EA5D426EFE72B0071837A /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 338EA5D326EFE72B0071837A /* RunnerTests.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 4CE8B69FE511476B98B4816C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B61FB9CDECD72211FAB708CA /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 338EA5D626EFE72B0071837A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 338EA5D126EFE72B0071837A /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 338EA5D326EFE72B0071837A /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 338EA5D526EFE72B0071837A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + B61FB9CDECD72211FAB708CA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BA03A11192D3E8EEA888D495 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + C03B6D624A05212E07A5D41E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + D921BBD60B6562B7A5F559AC /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 338EA5CE26EFE72B0071837A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4CE8B69FE511476B98B4816C /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 338EA5D226EFE72B0071837A /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 338EA5D326EFE72B0071837A /* RunnerTests.swift */, + 338EA5D526EFE72B0071837A /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 338EA5D226EFE72B0071837A /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + CAED34175B65FC224CC4F18C /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + 338EA5D126EFE72B0071837A /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + CAED34175B65FC224CC4F18C /* Pods */ = { + isa = PBXGroup; + children = ( + D921BBD60B6562B7A5F559AC /* Pods-Runner.debug.xcconfig */, + C03B6D624A05212E07A5D41E /* Pods-Runner.release.xcconfig */, + BA03A11192D3E8EEA888D495 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B61FB9CDECD72211FAB708CA /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 338EA5D026EFE72B0071837A /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 338EA5DB26EFE72B0071837A /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 338EA5CD26EFE72B0071837A /* Sources */, + 338EA5CE26EFE72B0071837A /* Frameworks */, + 338EA5CF26EFE72B0071837A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 338EA5D726EFE72B0071837A /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 338EA5D126EFE72B0071837A /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 8F1744F37738365955F17998 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + A8D2084B0509A3B3053F3AF7 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1250; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 338EA5D026EFE72B0071837A = { + CreatedOnToolsVersion = 12.5; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + 338EA5D026EFE72B0071837A /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 338EA5CF26EFE72B0071837A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 8F1744F37738365955F17998 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + A8D2084B0509A3B3053F3AF7 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 338EA5CD26EFE72B0071837A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 338EA5D426EFE72B0071837A /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 338EA5D726EFE72B0071837A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 338EA5D626EFE72B0071837A /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 338EA5D826EFE72B0071837A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/Contents/MacOS/example"; + }; + name = Debug; + }; + 338EA5D926EFE72B0071837A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/Contents/MacOS/example"; + }; + name = Release; + }; + 338EA5DA26EFE72B0071837A /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/Contents/MacOS/example"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 338EA5DB26EFE72B0071837A /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 338EA5D826EFE72B0071837A /* Debug */, + 338EA5D926EFE72B0071837A /* Release */, + 338EA5DA26EFE72B0071837A /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..57d6538229d5 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/file_selector/file_selector_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/file_selector/file_selector_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/AppDelegate.swift b/packages/file_selector/file_selector_macos/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000000..5cec4c48f620 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..a2ec33f19f11 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000000..3c4935a7ca84 Binary files /dev/null and b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000000..ed4cc1642168 Binary files /dev/null and b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000000..483be6138973 Binary files /dev/null and b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000000..bcbf36df2f2a Binary files /dev/null and b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000000..9c0a65286476 Binary files /dev/null and b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000000..e71a726136a4 Binary files /dev/null and b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000000..8a31fe2dd3f9 Binary files /dev/null and b/packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/file_selector/file_selector_macos/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000000..537341abf994 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000000..ef311e2bba6f --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.fileSelectorExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2020 The Flutter Authors. All rights reserved. diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Debug.xcconfig b/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000000..36b0fd9464f4 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Release.xcconfig b/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000000..dff4f49561c8 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Warnings.xcconfig b/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000000..42bcbf4780b1 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/DebugProfile.entitlements b/packages/file_selector/file_selector_macos/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000000..d138bd5b0451 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Info.plist b/packages/file_selector/file_selector_macos/example/macos/Runner/Info.plist new file mode 100644 index 000000000000..4789daa6a443 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/MainFlutterWindow.swift b/packages/file_selector/file_selector_macos/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000000..32aaeedceb1f --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/file_selector/file_selector_macos/example/macos/Runner/Release.entitlements b/packages/file_selector/file_selector_macos/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000000..19afff14a08c --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + + diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/Info.plist b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/Info.plist similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/Info.plist rename to packages/file_selector/file_selector_macos/example/macos/RunnerTests/Info.plist diff --git a/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000000..bffc3452c49d --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,283 @@ +// 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. + +@testable import file_selector_macos +import FlutterMacOS +import XCTest + +class TestPanelController: NSObject, PanelController { + // The last panels that the relevant display methods were called on. + public var savePanel: NSSavePanel? + public var openPanel: NSOpenPanel? + + // Mock return values for the display methods. + public var saveURL: URL? + public var openURLs: [URL]? + + func display(_ panel: NSSavePanel, for window: NSWindow?, completionHandler handler: @escaping (URL?) -> Void) { + savePanel = panel + handler(saveURL) + } + + func display(_ panel: NSOpenPanel, for window: NSWindow?, completionHandler handler: @escaping ([URL]?) -> Void) { + openPanel = panel + handler(openURLs) + } +} + +class TestViewProvider: NSObject, ViewProvider { + var view: NSView? { + get { + window?.contentView + } + } + var window: NSWindow? = NSWindow() +} + +class exampleTests: XCTestCase { + + func testOpenSimple() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let returnPath = "/foo/bar" + panelController.openURLs = [URL(fileURLWithPath: returnPath)] + + let called = XCTestExpectation() + let call = FlutterMethodCall(methodName: "openFile", arguments: [:]) + plugin.handle(call) { result in + XCTAssertEqual((result as! [String]?)![0], returnPath) + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + if let panel = panelController.openPanel { + XCTAssertTrue(panel.canChooseFiles) + // For consistency across platforms, directory selection is disabled. + XCTAssertFalse(panel.canChooseDirectories) + } + } + + func testOpenWithArguments() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let returnPath = "/foo/bar" + panelController.openURLs = [URL(fileURLWithPath: returnPath)] + + let called = XCTestExpectation() + let call = FlutterMethodCall( + methodName: "openFile", + arguments: [ + "initialDirectory": "/some/dir", + "suggestedName": "a name", + "confirmButtonText": "Open it!", + ] + ) + plugin.handle(call) { result in + XCTAssertEqual((result as! [String]?)![0], returnPath) + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + if let panel = panelController.openPanel { + XCTAssertEqual(panel.directoryURL?.path, "/some/dir") + XCTAssertEqual(panel.nameFieldStringValue, "a name") + XCTAssertEqual(panel.prompt, "Open it!") + } + } + + func testOpenMultiple() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let returnPaths = ["/foo/bar", "/foo/baz"] + panelController.openURLs = returnPaths.map({ path in URL(fileURLWithPath: path) }) + + let called = XCTestExpectation() + let call = FlutterMethodCall( + methodName: "openFile", + arguments: ["multiple": true] + ) + plugin.handle(call) { result in + let paths = (result as! [String]?)! + XCTAssertEqual(paths.count, returnPaths.count) + XCTAssertEqual(paths[0], returnPaths[0]) + XCTAssertEqual(paths[1], returnPaths[1]) + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + } + + func testOpenWithFilter() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let returnPath = "/foo/bar" + panelController.openURLs = [URL(fileURLWithPath: returnPath)] + + let called = XCTestExpectation() + let call = FlutterMethodCall( + methodName: "openFile", + arguments: [ + "acceptedTypes": [ + "extensions": ["txt", "json"], + "UTIs": ["public.text", "public.image"], + ] + ] + ) + plugin.handle(call) { result in + XCTAssertEqual((result as! [String]?)![0], returnPath) + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + if let panel = panelController.openPanel { + XCTAssertEqual(panel.allowedFileTypes, ["txt", "json", "public.text", "public.image"]) + } + } + + func testOpenCancel() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let called = XCTestExpectation() + let call = FlutterMethodCall(methodName: "openFile", arguments: [:]) + plugin.handle(call) { result in + XCTAssertNil(result) + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + } + + func testSaveSimple() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let returnPath = "/foo/bar" + panelController.saveURL = URL(fileURLWithPath: returnPath) + + let called = XCTestExpectation() + let call = FlutterMethodCall(methodName: "getSavePath", arguments: [:]) + plugin.handle(call) { result in + XCTAssertEqual(result as! String?, returnPath) + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.savePanel) + } + + func testSaveWithArguments() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let returnPath = "/foo/bar" + panelController.saveURL = URL(fileURLWithPath: returnPath) + + let called = XCTestExpectation() + let call = FlutterMethodCall( + methodName: "getSavePath", + arguments: [ + "initialDirectory": "/some/dir", + "confirmButtonText": "Save it!", + ] + ) + plugin.handle(call) { result in + XCTAssertEqual(result as! String?, returnPath) + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.savePanel) + if let panel = panelController.savePanel { + XCTAssertEqual(panel.directoryURL?.path, "/some/dir") + XCTAssertEqual(panel.prompt, "Save it!") + } + } + + func testSaveCancel() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let called = XCTestExpectation() + let call = FlutterMethodCall(methodName: "getSavePath", arguments: [:]) + plugin.handle(call) { result in + XCTAssertNil(result) + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.savePanel) + } + + func testGetDirectorySimple() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let returnPath = "/foo/bar" + panelController.openURLs = [URL(fileURLWithPath: returnPath)] + + let called = XCTestExpectation() + let call = FlutterMethodCall(methodName: "getDirectoryPath", arguments: [:]) + plugin.handle(call) { result in + XCTAssertEqual(result as! String?, returnPath) + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + if let panel = panelController.openPanel { + XCTAssertTrue(panel.canChooseDirectories) + // For consistency across platforms, file selection is disabled. + XCTAssertFalse(panel.canChooseFiles) + // The Dart API only allows a single directory to be returned, so users shouldn't be allowed + // to select multiple. + XCTAssertFalse(panel.allowsMultipleSelection) + } + } + + func testGetDirectoryCancel() throws { + let panelController = TestPanelController() + let plugin = FileSelectorPlugin( + viewProvider: TestViewProvider(), + panelController: panelController) + + let called = XCTestExpectation() + let call = FlutterMethodCall(methodName: "getDirectoryPath", arguments: [:]) + plugin.handle(call) { result in + XCTAssertNil(result) + called.fulfill() + } + + wait(for: [called], timeout: 0.5) + XCTAssertNotNil(panelController.openPanel) + } + +} diff --git a/packages/file_selector/file_selector_macos/example/pubspec.yaml b/packages/file_selector/file_selector_macos/example/pubspec.yaml new file mode 100644 index 000000000000..d3f3114bb481 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/pubspec.yaml @@ -0,0 +1,27 @@ +name: example +description: Example for file_selector_macos implementation. +publish_to: 'none' +version: 1.0.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + file_selector_macos: + # When depending on this package from a real application you should use: + # file_selector_macos: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: .. + file_selector_platform_interface: ^2.2.0 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart new file mode 100644 index 000000000000..74ce2835d18c --- /dev/null +++ b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart @@ -0,0 +1,129 @@ +// 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/foundation.dart' show visibleForTesting; +import 'package:flutter/services.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/file_selector_macos'); + +/// An implementation of [FileSelectorPlatform] for macOS. +class FileSelectorMacOS extends FileSelectorPlatform { + /// The MethodChannel that is being used by this implementation of the plugin. + @visibleForTesting + MethodChannel get channel => _channel; + + /// Registers the macOS implementation. + static void registerWith() { + FileSelectorPlatform.instance = FileSelectorMacOS(); + } + + @override + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final List? path = await _channel.invokeListMethod( + 'openFile', + { + 'acceptedTypes': _allowedTypeListFromTypeGroups(acceptedTypeGroups), + 'initialDirectory': initialDirectory, + 'confirmButtonText': confirmButtonText, + 'multiple': false, + }, + ); + return path == null ? null : XFile(path.first); + } + + @override + Future> openFiles({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final List? pathList = await _channel.invokeListMethod( + 'openFile', + { + 'acceptedTypes': _allowedTypeListFromTypeGroups(acceptedTypeGroups), + 'initialDirectory': initialDirectory, + 'confirmButtonText': confirmButtonText, + 'multiple': true, + }, + ); + return pathList?.map((String path) => XFile(path)).toList() ?? []; + } + + @override + Future getSavePath({ + List? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) async { + return _channel.invokeMethod( + 'getSavePath', + { + 'acceptedTypes': _allowedTypeListFromTypeGroups(acceptedTypeGroups), + 'initialDirectory': initialDirectory, + 'suggestedName': suggestedName, + 'confirmButtonText': confirmButtonText, + }, + ); + } + + @override + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) async { + return _channel.invokeMethod( + 'getDirectoryPath', + { + 'initialDirectory': initialDirectory, + 'confirmButtonText': confirmButtonText, + }, + ); + } + + // Converts the type group list into a flat list of all allowed types, since + // macOS doesn't support filter groups. + Map>? _allowedTypeListFromTypeGroups( + List? typeGroups) { + const String extensionKey = 'extensions'; + const String mimeTypeKey = 'mimeTypes'; + const String utiKey = 'UTIs'; + if (typeGroups == null || typeGroups.isEmpty) { + return null; + } + final Map> allowedTypes = >{ + extensionKey: [], + mimeTypeKey: [], + utiKey: [], + }; + for (final XTypeGroup typeGroup in typeGroups) { + // If any group allows everything, no filtering should be done. + if (typeGroup.allowsAny) { + return null; + } + // Reject a filter that isn't an allow-any, but doesn't set any + // macOS-supported filter categories. + if ((typeGroup.extensions?.isEmpty ?? true) && + (typeGroup.macUTIs?.isEmpty ?? true) && + (typeGroup.mimeTypes?.isEmpty ?? true)) { + throw ArgumentError('Provided type group $typeGroup does not allow ' + 'all files, but does not set any of the macOS-supported filter ' + 'categories. At least one of "extensions", "macUTIs", or ' + '"mimeTypes" must be non-empty for macOS if anything is ' + 'non-empty.'); + } + allowedTypes[extensionKey]!.addAll(typeGroup.extensions ?? []); + allowedTypes[mimeTypeKey]!.addAll(typeGroup.mimeTypes ?? []); + allowedTypes[utiKey]!.addAll(typeGroup.macUTIs ?? []); + } + + return allowedTypes; + } +} diff --git a/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift new file mode 100644 index 000000000000..9551671d1575 --- /dev/null +++ b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift @@ -0,0 +1,218 @@ +// 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 FlutterMacOS +import Foundation + +/// Protocol for showing panels, allowing for depenedency injection in tests. +protocol PanelController { + /// Displays the given save panel, and provides the selected URL, or nil if the panel is + /// cancelled, to the handler. + /// - Parameters: + /// - panel: The panel to show. + /// - window: The window to display the panel for. + /// - completionHandler: The completion handler to receive the results. + func display( + _ panel: NSSavePanel, + for window: NSWindow?, + completionHandler: @escaping (URL?) -> Void); + + /// Displays the given open panel, and provides the selected URLs, or nil if the panel is + /// cancelled, to the handler. + /// - Parameters: + /// - panel: The panel to show. + /// - window: The window to display the panel for. + /// - completionHandler: The completion handler to receive the results. + func display( + _ panel: NSOpenPanel, + for window: NSWindow?, + completionHandler: @escaping ([URL]?) -> Void); +} + +/// Protocol to provide access to the Flutter view, allowing for dependency injection in tests. +/// +/// This is necessary because Swift doesn't allow for only partially implementing a protocol, so +/// a stub implementation of FlutterPluginRegistrar for tests would break any time something was +/// added to that protocol. +protocol ViewProvider { + /// Returns the view associated with the Flutter content. + var view: NSView? { get } +} + +public class FileSelectorPlugin: NSObject, FlutterPlugin { + private let viewProvider: ViewProvider + private let panelController: PanelController + + private let openMethod = "openFile" + private let openDirectoryMethod = "getDirectoryPath" + private let saveMethod = "getSavePath" + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel( + name: "plugins.flutter.io/file_selector_macos", + binaryMessenger: registrar.messenger) + let instance = FileSelectorPlugin( + viewProvider: DefaultViewProvider(registrar: registrar), + panelController: DefaultPanelController()) + registrar.addMethodCallDelegate(instance, channel: channel) + } + + init(viewProvider: ViewProvider, panelController: PanelController) { + self.viewProvider = viewProvider + self.panelController = panelController + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = (call.arguments ?? [:]) as! [String: Any] + switch call.method { + case openMethod, + openDirectoryMethod: + let choosingDirectory = call.method == openDirectoryMethod + let panel = NSOpenPanel() + configure(panel: panel, with: arguments) + configure(openPanel: panel, with: arguments, choosingDirectory: choosingDirectory) + panelController.display(panel, for: viewProvider.view?.window) { (selection: [URL]?) in + if (choosingDirectory) { + result(selection?.first?.path) + } else { + result(selection?.map({ item in item.path })) + } + } + case saveMethod: + let panel = NSSavePanel() + configure(panel: panel, with: arguments) + panelController.display(panel, for: viewProvider.view?.window) { (selection: URL?) in + result(selection?.path) + } + default: + result(FlutterMethodNotImplemented) + } + } + + /// Configures an NSSavePanel based on channel method call arguments. + /// - Parameters: + /// - panel: The panel to configure. + /// - arguments: The arguments dictionary from a FlutterMethodCall to this plugin. + private func configure(panel: NSSavePanel, with arguments: [String: Any]) { + if let initialDirectory = getNonNullStringValue(for: "initialDirectory", from: arguments) { + panel.directoryURL = URL(fileURLWithPath: initialDirectory) + } + if let suggestedName = getNonNullStringValue(for: "suggestedName", from: arguments) { + panel.nameFieldStringValue = suggestedName + } + if let confirmButtonText = getNonNullStringValue(for: "confirmButtonText", from: arguments) { + panel.prompt = confirmButtonText + } + + let acceptedTypes = getNonNullValue( + for: "acceptedTypes", + from: arguments + ) as! [String: Any]? + if let acceptedTypes = acceptedTypes { + var allowedTypes: [String] = [] + let extensions = getNonNullStringArrayValue(for: "extensions", from: acceptedTypes) + let UTIs = getNonNullStringArrayValue(for: "UTIs", from: acceptedTypes) + allowedTypes.append(contentsOf: extensions) + allowedTypes.append(contentsOf: UTIs) + // TODO: Add support for mimeTypes in macOS 11+. + + if !allowedTypes.isEmpty { + panel.allowedFileTypes = allowedTypes + } + } + } + + /// Configures an NSOpenPanel based on channel method call arguments. + /// - Parameters: + /// - panel: The panel to configure. + /// - arguments: The arguments dictionary from a FlutterMethodCall to this plugin. + /// - choosingDirectory: True if the panel should allow choosing directories rather than files. + private func configure( + openPanel panel: NSOpenPanel, + with arguments: [String: Any], + choosingDirectory: Bool + ) { + panel.allowsMultipleSelection = + getNonNullValue(for: "multiple", from: arguments) as! Bool? ?? false + panel.canChooseDirectories = choosingDirectory; + panel.canChooseFiles = !choosingDirectory; + } +} + +/// Non-test implementation of PanelController that calls the standard methods to display the panel +/// either as a sheet (if a window is provided) or modal (if not). +private class DefaultPanelController: PanelController { + func display( + _ panel: NSSavePanel, + for window: NSWindow?, + completionHandler: @escaping (URL?) -> Void + ) { + let completionAdapter = { response in + completionHandler((response == NSApplication.ModalResponse.OK) ? panel.url : nil) + } + if let window = window { + panel.beginSheetModal(for: window, completionHandler: completionAdapter) + } else { + completionAdapter(panel.runModal()) + } + } + + func display( + _ panel: NSOpenPanel, + for window: NSWindow?, + completionHandler: @escaping ([URL]?) -> Void + ) { + let completionAdapter = { response in + completionHandler((response == NSApplication.ModalResponse.OK) ? panel.urls : nil) + } + if let window = window { + panel.beginSheetModal(for: window, completionHandler: completionAdapter) + } else { + completionAdapter(panel.runModal()) + } + } +} + +/// Non-test implementation of PanelController that forwards to the plugin registrar. +private class DefaultViewProvider: ViewProvider { + private let registrar: FlutterPluginRegistrar + + init(registrar: FlutterPluginRegistrar) { + self.registrar = registrar + } + + var view: NSView? { + get { + registrar.view + } + } +} + +/// Returns the value for the given key from the provided dictionary, unless the value is NSNull +/// in which case it returns nil. +/// - Parameters: +/// - key: The key to get a value for. +/// - dictionary: The dictionary to get the value from. +/// - Returns: The value, or nil for NSNull. +private func getNonNullValue(for key: String, from dictionary: [String: Any]) -> Any? { + let value = dictionary[key]; + return value is NSNull ? nil : value; +} + +/// A convenience wrapper for getNonNullValue for string values. +private func getNonNullStringValue(for key: String, from dictionary: [String: Any]) -> String? { + return getNonNullValue(for: key, from: dictionary) as! String? +} + +/// A convenience wrapper for getNonNullValue for array-of-string values. +/// - Parameters: +/// - key: The key to get a value for. +/// - dictionary: The dictionary to get the value from. +/// - Returns: The value, or an empty array for nil for NSNull. +private func getNonNullStringArrayValue( + for key: String, + from dictionary: [String: Any] +) -> [String] { + return getNonNullValue(for: key, from: dictionary) as! [String]? ?? [] +} diff --git a/packages/file_selector/file_selector_macos/macos/file_selector_macos.podspec b/packages/file_selector/file_selector_macos/macos/file_selector_macos.podspec new file mode 100644 index 000000000000..3533c3a422ec --- /dev/null +++ b/packages/file_selector/file_selector_macos/macos/file_selector_macos.podspec @@ -0,0 +1,21 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'file_selector_macos' + s.version = '0.0.1' + s.summary = 'macOS implementation of file_selector.' + s.description = <<-DESC +Displays native macOS open and save panels. + DESC + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/file_selector' + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_macos' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' +end diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml new file mode 100644 index 000000000000..3fc3832d7280 --- /dev/null +++ b/packages/file_selector/file_selector_macos/pubspec.yaml @@ -0,0 +1,27 @@ +name: file_selector_macos +description: macOS implementation of the file_selector plugin. +repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_macos +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 +version: 0.9.0+3 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: file_selector + platforms: + macos: + dartPluginClass: FileSelectorMacOS + pluginClass: FileSelectorPlugin + +dependencies: + cross_file: ^0.3.1 + file_selector_platform_interface: ^2.2.0 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart new file mode 100644 index 000000000000..789d70a51777 --- /dev/null +++ b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart @@ -0,0 +1,358 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_macos/file_selector_macos.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final FileSelectorMacOS plugin = FileSelectorMacOS(); + + final List log = []; + + setUp(() { + plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return null; + }); + + log.clear(); + }); + + test('registered instance', () { + FileSelectorMacOS.registerWith(); + expect(FileSelectorPlatform.instance, isA()); + }); + + group('openFile', () { + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); + + await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypes': { + 'extensions': ['txt', 'jpg'], + 'mimeTypes': ['text/plain', 'image/jpg'], + 'UTIs': ['public.text', 'public.image'], + }, + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }), + ], + ); + }); + + test('passes initialDirectory correctly', () async { + await plugin.openFile(initialDirectory: '/example/directory'); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypes': null, + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': false, + }), + ], + ); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.openFile(confirmButtonText: 'Open File'); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypes': null, + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': false, + }), + ], + ); + }); + + test('throws for a type group that does not support macOS', () async { + const XTypeGroup group = XTypeGroup( + label: 'images', + webWildCards: ['images/*'], + ); + + await expectLater( + plugin.openFile(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('allows a wildcard group', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + ); + + await expectLater( + plugin.openFile(acceptedTypeGroups: [group]), completes); + }); + }); + + group('openFiles', () { + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); + + await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypes': >{ + 'extensions': ['txt', 'jpg'], + 'mimeTypes': ['text/plain', 'image/jpg'], + 'UTIs': ['public.text', 'public.image'], + }, + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': true, + }), + ], + ); + }); + + test('passes initialDirectory correctly', () async { + await plugin.openFiles(initialDirectory: '/example/directory'); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypes': null, + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': true, + }), + ], + ); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.openFiles(confirmButtonText: 'Open File'); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypes': null, + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': true, + }), + ], + ); + }); + + test('throws for a type group that does not support macOS', () async { + const XTypeGroup group = XTypeGroup( + label: 'images', + webWildCards: ['images/*'], + ); + + await expectLater( + plugin.openFiles(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('allows a wildcard group', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + ); + + await expectLater( + plugin.openFiles(acceptedTypeGroups: [group]), completes); + }); + }); + + group('getSavePath', () { + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); + + await plugin + .getSavePath(acceptedTypeGroups: [group, groupTwo]); + + expect( + log, + [ + isMethodCall('getSavePath', arguments: { + 'acceptedTypes': >{ + 'extensions': ['txt', 'jpg'], + 'mimeTypes': ['text/plain', 'image/jpg'], + 'UTIs': ['public.text', 'public.image'], + }, + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': null, + }), + ], + ); + }); + + test('passes initialDirectory correctly', () async { + await plugin.getSavePath(initialDirectory: '/example/directory'); + + expect( + log, + [ + isMethodCall('getSavePath', arguments: { + 'acceptedTypes': null, + 'initialDirectory': '/example/directory', + 'suggestedName': null, + 'confirmButtonText': null, + }), + ], + ); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.getSavePath(confirmButtonText: 'Open File'); + + expect( + log, + [ + isMethodCall('getSavePath', arguments: { + 'acceptedTypes': null, + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': 'Open File', + }), + ], + ); + }); + + test('throws for a type group that does not support macOS', () async { + const XTypeGroup group = XTypeGroup( + label: 'images', + webWildCards: ['images/*'], + ); + + await expectLater( + plugin.getSavePath(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('allows a wildcard group', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + ); + + await expectLater( + plugin.getSavePath(acceptedTypeGroups: [group]), + completes); + }); + }); + + group('getDirectoryPath', () { + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPath(initialDirectory: '/example/directory'); + + expect( + log, + [ + isMethodCall('getDirectoryPath', arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + }), + ], + ); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPath(confirmButtonText: 'Open File'); + + expect( + log, + [ + isMethodCall('getDirectoryPath', arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + }), + ], + ); + }); + }); + + test('ignores all type groups if any of them is a wildcard', () async { + await plugin.getSavePath(acceptedTypeGroups: [ + const XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ), + const XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + ), + const XTypeGroup( + label: 'any', + ), + ]); + + expect( + log, + [ + isMethodCall('getSavePath', arguments: { + 'acceptedTypes': null, + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': null, + }), + ], + ); + }); +} diff --git a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md index b633bd35a59e..ad803fb12e66 100644 --- a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md +++ b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md @@ -1,3 +1,21 @@ +## 2.3.0 + +* Replaces `macUTIs` with `uniformTypeIdentifiers`. `macUTIs` is available as an alias, but will be deprecated in a future release. + +## 2.2.0 + +* Makes `XTypeGroup`'s constructor constant. + +## 2.1.1 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. + +## 2.1.0 + +* Adds `allowsAny` to `XTypeGroup` as a simple and future-proof way of identifying + wildcard groups. + ## 2.0.4 * Removes dependency on `meta`. diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart b/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart index f1fa82b6f3c6..d6aebd01730f 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart @@ -2,11 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:cross_file/cross_file.dart'; -import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/services.dart'; +import '../../file_selector_platform_interface.dart'; + const MethodChannel _channel = MethodChannel('plugins.flutter.io/file_selector'); diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart b/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart index f8fa83bd18d2..eb4563c47917 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart @@ -4,10 +4,9 @@ import 'dart:async'; -import 'package:cross_file/cross_file.dart'; -import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import '../../file_selector_platform_interface.dart'; import '../method_channel/method_channel_file_selector.dart'; /// The interface that implementations of file_selector must implement. diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart index 2146131023e1..e12b431d91d8 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart @@ -1,37 +1,47 @@ // 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' show immutable; -/// A set of allowed XTypes +/// A set of allowed XTypes. +@immutable class XTypeGroup { /// Creates a new group with the given label and file extensions. /// /// A group with none of the type options provided indicates that any type is /// allowed. - XTypeGroup({ + const XTypeGroup({ this.label, List? extensions, this.mimeTypes, - this.macUTIs, + List? macUTIs, + List? uniformTypeIdentifiers, this.webWildCards, - }) : extensions = _removeLeadingDots(extensions); + }) : _extensions = extensions, + assert(uniformTypeIdentifiers == null || macUTIs == null, + 'Only one of uniformTypeIdentifiers or macUTIs can be non-null'), + uniformTypeIdentifiers = uniformTypeIdentifiers ?? macUTIs; - /// The 'name' or reference to this group of types + /// The 'name' or reference to this group of types. final String? label; - /// The extensions for this group - final List? extensions; - - /// The MIME types for this group + /// The MIME types for this group. final List? mimeTypes; - /// The UTIs for this group - final List? macUTIs; + /// The uniform type identifiers for this group + final List? uniformTypeIdentifiers; - /// The web wild cards for this group (ex: image/*, video/*) + /// The web wild cards for this group (ex: image/*, video/*). final List? webWildCards; - /// Converts this object into a JSON formatted object + final List? _extensions; + + /// The extensions for this group. + List? get extensions { + return _removeLeadingDots(_extensions); + } + + /// Converts this object into a JSON formatted object. Map toJSON() { return { 'label': label, @@ -42,6 +52,17 @@ class XTypeGroup { }; } + /// True if this type group should allow any file. + bool get allowsAny { + return (extensions?.isEmpty ?? true) && + (mimeTypes?.isEmpty ?? true) && + (macUTIs?.isEmpty ?? true) && + (webWildCards?.isEmpty ?? true); + } + + /// Returns the list of uniform type identifiers for this group + List? get macUTIs => uniformTypeIdentifiers; + static List? _removeLeadingDots(List? exts) => exts ?.map((String ext) => ext.startsWith('.') ? ext.substring(1) : ext) .toList(); diff --git a/packages/file_selector/file_selector_platform_interface/pubspec.yaml b/packages/file_selector/file_selector_platform_interface/pubspec.yaml index 42917751ed58..ac8727c09e36 100644 --- a/packages/file_selector/file_selector_platform_interface/pubspec.yaml +++ b/packages/file_selector/file_selector_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%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.0.4 +version: 2.3.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" dependencies: cross_file: ^0.3.0 @@ -20,5 +20,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 test: ^1.16.3 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 33f9fbf45a8b..0f5f3a17ae0c 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 @@ -26,14 +26,14 @@ void main() { group('#openFile', () { test('passes the accepted type groups correctly', () async { - final XTypeGroup group = XTypeGroup( + const XTypeGroup group = XTypeGroup( label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], macUTIs: ['public.text'], ); - final XTypeGroup groupTwo = XTypeGroup( + const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], @@ -91,14 +91,14 @@ void main() { }); group('#openFiles', () { test('passes the accepted type groups correctly', () async { - final XTypeGroup group = XTypeGroup( + const XTypeGroup group = XTypeGroup( label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], macUTIs: ['public.text'], ); - final XTypeGroup groupTwo = XTypeGroup( + const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], @@ -157,14 +157,14 @@ void main() { group('#getSavePath', () { test('passes the accepted type groups correctly', () async { - final XTypeGroup group = XTypeGroup( + const XTypeGroup group = XTypeGroup( label: 'text', extensions: ['txt'], mimeTypes: ['text/plain'], macUTIs: ['public.text'], ); - final XTypeGroup groupTwo = XTypeGroup( + const XTypeGroup groupTwo = XTypeGroup( label: 'image', extensions: ['jpg'], mimeTypes: ['image/jpg'], diff --git a/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart b/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart index 84f5ca1f0bd2..5ac5722716c7 100644 --- a/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart +++ b/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart @@ -8,13 +8,12 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('XTypeGroup', () { test('toJSON() creates correct map', () { + const List extensions = ['txt', 'jpg']; + const List mimeTypes = ['text/plain']; + const List macUTIs = ['public.plain-text']; + const List webWildCards = ['image/*']; const String label = 'test group'; - final List extensions = ['txt', 'jpg']; - final List mimeTypes = ['text/plain']; - final List macUTIs = ['public.plain-text']; - final List webWildCards = ['image/*']; - - final XTypeGroup group = XTypeGroup( + const XTypeGroup group = XTypeGroup( label: label, extensions: extensions, mimeTypes: mimeTypes, @@ -30,8 +29,8 @@ void main() { expect(jsonMap['webWildCards'], webWildCards); }); - test('A wildcard group can be created', () { - final XTypeGroup group = XTypeGroup( + test('a wildcard group can be created', () { + const XTypeGroup group = XTypeGroup( label: 'Any', ); @@ -40,11 +39,103 @@ void main() { expect(jsonMap['mimeTypes'], null); expect(jsonMap['macUTIs'], null); expect(jsonMap['webWildCards'], null); + expect(group.allowsAny, true); + }); + + test('allowsAny treats empty arrays the same as null', () { + const XTypeGroup group = XTypeGroup( + label: 'Any', + extensions: [], + mimeTypes: [], + macUTIs: [], + webWildCards: [], + ); + + expect(group.allowsAny, true); + }); + + test('allowsAny returns false if anything is set', () { + const XTypeGroup extensionOnly = + XTypeGroup(label: 'extensions', extensions: ['txt']); + const XTypeGroup mimeOnly = + XTypeGroup(label: 'mime', mimeTypes: ['text/plain']); + const XTypeGroup utiOnly = + XTypeGroup(label: 'utis', macUTIs: ['public.text']); + const XTypeGroup webOnly = + XTypeGroup(label: 'web', webWildCards: ['.txt']); + + expect(extensionOnly.allowsAny, false); + expect(mimeOnly.allowsAny, false); + expect(utiOnly.allowsAny, false); + expect(webOnly.allowsAny, false); + }); + + test('passing only macUTIs should fill uniformTypeIdentifiers', () { + const List macUTIs = ['public.plain-text']; + const XTypeGroup group = XTypeGroup( + macUTIs: macUTIs, + ); + + expect(group.uniformTypeIdentifiers, macUTIs); + }); + + test( + 'passing only uniformTypeIdentifiers should fill uniformTypeIdentifiers', + () { + const List uniformTypeIdentifiers = ['public.plain-text']; + const XTypeGroup group = XTypeGroup( + uniformTypeIdentifiers: uniformTypeIdentifiers, + ); + + expect(group.uniformTypeIdentifiers, uniformTypeIdentifiers); + }); + + test('macUTIs getter return macUTIs value passed in constructor', () { + const List macUTIs = ['public.plain-text']; + const XTypeGroup group = XTypeGroup( + macUTIs: macUTIs, + ); + + expect(group.macUTIs, macUTIs); + }); + + test( + 'macUTIs getter returns uniformTypeIdentifiers value passed in constructor', + () { + const List uniformTypeIdentifiers = ['public.plain-text']; + const XTypeGroup group = XTypeGroup( + uniformTypeIdentifiers: uniformTypeIdentifiers, + ); + + expect(group.macUTIs, uniformTypeIdentifiers); + }); + + test('passing both uniformTypeIdentifiers and macUTIs should throw', () { + const List macUTIs = ['public.plain-text']; + const List uniformTypeIndentifiers = [ + 'public.plain-images' + ]; + expect( + () => XTypeGroup( + macUTIs: macUTIs, + uniformTypeIdentifiers: uniformTypeIndentifiers), + throwsA(predicate((Object? e) => + e is AssertionError && + e.message == + 'Only one of uniformTypeIdentifiers or macUTIs can be non-null'))); + }); + + test( + 'having uniformTypeIdentifiers and macUTIs as null should leave uniformTypeIdentifiers as null', + () { + const XTypeGroup group = XTypeGroup(); + + expect(group.uniformTypeIdentifiers, null); }); - test('Leading dots are removed from extensions', () { - final List extensions = ['.txt', '.jpg']; - final XTypeGroup group = XTypeGroup(extensions: extensions); + test('leading dots are removed from extensions', () { + const List extensions = ['.txt', '.jpg']; + const XTypeGroup group = XTypeGroup(extensions: extensions); expect(group.extensions, ['txt', 'jpg']); }); diff --git a/packages/file_selector/file_selector_web/CHANGELOG.md b/packages/file_selector/file_selector_web/CHANGELOG.md index 5927239ef9e3..5e531bb633d2 100644 --- a/packages/file_selector/file_selector_web/CHANGELOG.md +++ b/packages/file_selector/file_selector_web/CHANGELOG.md @@ -1,3 +1,28 @@ +## 0.9.0+2 + +* Changes XTypeGroup initialization from final to const. + +## 0.9.0+1 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 0.9.0 + +* **BREAKING CHANGE**: Methods that take `XTypeGroup`s now throw an + `ArgumentError` if any group is not a wildcard (all filter types null or + empty), but doesn't include any of the filter types supported by web. + +## 0.8.1+5 + +* Minor fixes for new analysis options. + +## 0.8.1+4 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 0.8.1+3 * Minor code cleanup for new analysis rules. diff --git a/packages/file_selector/file_selector_web/example/README.md b/packages/file_selector/file_selector_web/example/README.md index 8a6e74b107ea..0e51ae5ecbd2 100644 --- a/packages/file_selector/file_selector_web/example/README.md +++ b/packages/file_selector/file_selector_web/example/README.md @@ -1,4 +1,14 @@ -# Testing +# 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. + +## Testing This package uses `package:integration_test` to run its tests in a web browser. @@ -6,4 +16,4 @@ See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Te in the Flutter wiki for instructions to setup and run the tests in this package. Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) -for more info. \ No newline at end of file +for more info. diff --git a/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_test.dart b/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_test.dart index fe57d1d1e15d..664c40871f49 100644 --- a/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_test.dart +++ b/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_test.dart @@ -19,15 +19,14 @@ void main() { testWidgets('works', (WidgetTester _) async { final XFile mockFile = createXFile('1001', 'identity.png'); - final MockDomHelper mockDomHelper = MockDomHelper() - ..setFiles([mockFile]) - ..expectAccept('.jpg,.jpeg,image/png,image/*') - ..expectMultiple(false); + final MockDomHelper mockDomHelper = MockDomHelper( + files: [mockFile], + expectAccept: '.jpg,.jpeg,image/png,image/*'); final FileSelectorWeb plugin = FileSelectorWeb(domHelper: mockDomHelper); - final XTypeGroup typeGroup = XTypeGroup( + const XTypeGroup typeGroup = XTypeGroup( label: 'images', extensions: ['jpg', 'jpeg'], mimeTypes: ['image/png'], @@ -49,15 +48,15 @@ void main() { final XFile mockFile1 = createXFile('123456', 'file1.txt'); final XFile mockFile2 = createXFile('', 'file2.txt'); - final MockDomHelper mockDomHelper = MockDomHelper() - ..setFiles([mockFile1, mockFile2]) - ..expectAccept('.txt') - ..expectMultiple(true); + final MockDomHelper mockDomHelper = MockDomHelper( + files: [mockFile1, mockFile2], + expectAccept: '.txt', + expectMultiple: true); final FileSelectorWeb plugin = FileSelectorWeb(domHelper: mockDomHelper); - final XTypeGroup typeGroup = XTypeGroup( + const XTypeGroup typeGroup = XTypeGroup( label: 'files', extensions: ['.txt'], ); @@ -90,9 +89,17 @@ void main() { } class MockDomHelper implements DomHelper { - List _files = []; - String _expectedAccept = ''; - bool _expectedMultiple = false; + MockDomHelper({ + List files = const [], + String expectAccept = '', + bool expectMultiple = false, + }) : _files = files, + _expectedAccept = expectAccept, + _expectedMultiple = expectMultiple; + + final List _files; + final String _expectedAccept; + final bool _expectedMultiple; @override Future> getFiles({ @@ -106,18 +113,6 @@ class MockDomHelper implements DomHelper { reason: 'Expected "multiple" value does not match.'); return Future>.value(_files); } - - void setFiles(List files) { - _files = files; - } - - void expectAccept(String accept) { - _expectedAccept = accept; - } - - void expectMultiple(bool multiple) { - _expectedMultiple = multiple; - } } XFile createXFile(String content, String name) { diff --git a/packages/file_selector/file_selector_web/example/lib/main.dart b/packages/file_selector/file_selector_web/example/lib/main.dart index 341913a18490..87422953de6a 100644 --- a/packages/file_selector/file_selector_web/example/lib/main.dart +++ b/packages/file_selector/file_selector_web/example/lib/main.dart @@ -5,13 +5,16 @@ import 'package:flutter/material.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } /// App for testing class MyApp extends StatefulWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { diff --git a/packages/file_selector/file_selector_web/example/pubspec.yaml b/packages/file_selector/file_selector_web/example/pubspec.yaml index 8998587615e5..e14f5c2eedea 100644 --- a/packages/file_selector/file_selector_web/example/pubspec.yaml +++ b/packages/file_selector/file_selector_web/example/pubspec.yaml @@ -3,15 +3,16 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.2.0" + flutter: ">=2.10.0" dependencies: + file_selector_platform_interface: ^2.2.0 + file_selector_web: + path: ../ flutter: sdk: flutter dev_dependencies: - file_selector_web: - path: ../ flutter_driver: sdk: flutter flutter_test: diff --git a/packages/file_selector/file_selector_web/lib/file_selector_web.dart b/packages/file_selector/file_selector_web/lib/file_selector_web.dart index 915a2a806496..748bb3aa0df0 100644 --- a/packages/file_selector/file_selector_web/lib/file_selector_web.dart +++ b/packages/file_selector/file_selector_web/lib/file_selector_web.dart @@ -5,11 +5,12 @@ import 'dart:async'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; -import 'package:file_selector_web/src/dom_helper.dart'; -import 'package:file_selector_web/src/utils.dart'; import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'src/dom_helper.dart'; +import 'src/utils.dart'; + /// The web implementation of [FileSelectorPlatform]. /// /// This class implements the `package:file_selector` functionality for the web. diff --git a/packages/file_selector/file_selector_web/lib/src/utils.dart b/packages/file_selector/file_selector_web/lib/src/utils.dart index 6a534645fda6..7a7aa7a69509 100644 --- a/packages/file_selector/file_selector_web/lib/src/utils.dart +++ b/packages/file_selector/file_selector_web/lib/src/utils.dart @@ -11,7 +11,11 @@ String acceptedTypesToString(List? acceptedTypes) { } final List allTypes = []; for (final XTypeGroup group in acceptedTypes) { - _assertTypeGroupIsValid(group); + // If any group allows everything, no filtering should be done. + if (group.allowsAny) { + return ''; + } + _validateTypeGroup(group); if (group.extensions != null) { allTypes.addAll(group.extensions!.map(_normalizeExtension)); } @@ -25,16 +29,20 @@ String acceptedTypesToString(List? acceptedTypes) { return allTypes.join(','); } -/// Make sure that at least one of its fields is populated. -void _assertTypeGroupIsValid(XTypeGroup group) { - assert( - !((group.extensions == null || group.extensions!.isEmpty) && - (group.mimeTypes == null || group.mimeTypes!.isEmpty) && - (group.webWildCards == null || group.webWildCards!.isEmpty)), - 'At least one of extensions / mimeTypes / webWildCards is required for web.'); +/// Make sure that at least one of the supported fields is populated. +void _validateTypeGroup(XTypeGroup group) { + if ((group.extensions?.isEmpty ?? true) && + (group.mimeTypes?.isEmpty ?? true) && + (group.webWildCards?.isEmpty ?? true)) { + throw ArgumentError('Provided type group $group does not allow ' + 'all files, but does not set any of the web-supported filter ' + 'categories. At least one of "extensions", "mimeTypes", or ' + '"webWildCards" must be non-empty for web if anything is ' + 'non-empty.'); + } } /// Append a dot at the beggining if it is not there png -> .png String _normalizeExtension(String ext) { - return ext.isNotEmpty && ext[0] != '.' ? '.' + ext : ext; + return ext.isNotEmpty && ext[0] != '.' ? '.$ext' : ext; } diff --git a/packages/file_selector/file_selector_web/pubspec.yaml b/packages/file_selector/file_selector_web/pubspec.yaml index 74d0412b440f..848a41b754af 100644 --- a/packages/file_selector/file_selector_web/pubspec.yaml +++ b/packages/file_selector/file_selector_web/pubspec.yaml @@ -2,11 +2,11 @@ name: file_selector_web description: Web platform implementation of file_selector repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.8.1+3 +version: 0.9.0+2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: @@ -17,7 +17,7 @@ flutter: fileName: file_selector_web.dart dependencies: - file_selector_platform_interface: ^2.0.0 + file_selector_platform_interface: ^2.2.0 flutter: sdk: flutter flutter_web_plugins: @@ -26,4 +26,3 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/file_selector/file_selector_web/test/utils_test.dart b/packages/file_selector/file_selector_web/test/utils_test.dart index 9bddfd2e6304..f9f3a41295f0 100644 --- a/packages/file_selector/file_selector_web/test/utils_test.dart +++ b/packages/file_selector/file_selector_web/test/utils_test.dart @@ -10,7 +10,7 @@ void main() { group('FileSelectorWeb utils', () { group('acceptedTypesToString', () { test('works', () { - final List acceptedTypes = [ + const List acceptedTypes = [ XTypeGroup(label: 'images', webWildCards: ['images/*']), XTypeGroup(label: 'jpgs', extensions: ['jpg', 'jpeg']), XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), @@ -20,13 +20,13 @@ void main() { }); test('works with an empty list', () { - final List acceptedTypes = []; + const List acceptedTypes = []; final String accepts = acceptedTypesToString(acceptedTypes); expect(accepts, ''); }); test('works with extensions', () { - final List acceptedTypes = [ + const List acceptedTypes = [ XTypeGroup(label: 'jpgs', extensions: ['jpeg', 'jpg']), XTypeGroup(label: 'pngs', extensions: ['png']), ]; @@ -35,7 +35,7 @@ void main() { }); test('works with mime types', () { - final List acceptedTypes = [ + const List acceptedTypes = [ XTypeGroup( label: 'jpgs', mimeTypes: ['image/jpeg', 'image/jpg']), XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), @@ -45,7 +45,7 @@ void main() { }); test('works with web wild cards', () { - final List acceptedTypes = [ + const List acceptedTypes = [ XTypeGroup(label: 'images', webWildCards: ['image/*']), XTypeGroup(label: 'audios', webWildCards: ['audio/*']), XTypeGroup(label: 'videos', webWildCards: ['video/*']), @@ -53,6 +53,13 @@ void main() { final String accepts = acceptedTypesToString(acceptedTypes); expect(accepts, 'image/*,audio/*,video/*'); }); + + test('throws for a type group that does not support web', () { + const List acceptedTypes = [ + XTypeGroup(label: 'text', macUTIs: ['public.text']), + ]; + expect(() => acceptedTypesToString(acceptedTypes), throwsArgumentError); + }); }); }); } diff --git a/packages/file_selector/file_selector_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md index 63999f245d82..13e895ca46f1 100644 --- a/packages/file_selector/file_selector_windows/CHANGELOG.md +++ b/packages/file_selector/file_selector_windows/CHANGELOG.md @@ -1,3 +1,42 @@ +## 0.9.1+4 + +* Changes XTypeGroup initialization from final to const. + +## 0.9.1+3 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. + +## 0.9.1+2 + +* Fixes the problem that the initial directory does not work after completing a file selection. + +## 0.9.1+1 + +* Updates README for endorsement. +* Updates `flutter_test` to be a `dev_dependencies` entry. + +## 0.9.1 + +* Converts the method channel to Pigeon. + +## 0.9.0 + +* **BREAKING CHANGE**: Methods that take `XTypeGroup`s now throw an + `ArgumentError` if any group is not a wildcard (all filter types null or + empty), but doesn't include any of the filter types supported by Windows. +* Ignores deprecation warnings for upcoming styleFrom button API changes. + +## 0.8.2+2 + +* Updates references to the obsolete master branch. + +## 0.8.2+1 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 0.8.2 * Moves source to flutter/plugins, and restructures to allow for unit testing. diff --git a/packages/file_selector/file_selector_windows/README.md b/packages/file_selector/file_selector_windows/README.md index 69fb088d599e..c597d704cadb 100644 --- a/packages/file_selector/file_selector_windows/README.md +++ b/packages/file_selector/file_selector_windows/README.md @@ -4,16 +4,8 @@ The Windows implementation of [`file_selector`][1]. ## Usage -### Importing the package - -This implementation has not yet been endorsed, meaning that you need to -[depend on `file_selector_windows`][2] in addition to -[depending on `file_selector`][3]. - -Once your pubspec includes the Windows implementation, you can use the -`file_selector` APIs normally. You should not use the `file_selector_windows` -APIs directly. +This package is [endorsed][2], which means you can simply use `file_selector` +normally. This package will be automatically included in your app when you do. [1]: https://pub.dev/packages/file_selector -[2]: https://pub.dev/packages/file_selector_windows/install -[3]: https://pub.dev/packages/file_selector/install +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/file_selector/file_selector_windows/example/README.md b/packages/file_selector/file_selector_windows/example/README.md index c8a3cce44a9a..96b8bb17dbff 100644 --- a/packages/file_selector/file_selector_windows/example/README.md +++ b/packages/file_selector/file_selector_windows/example/README.md @@ -1,4 +1,9 @@ -# `file_selector_windows` Example +# Platform Implementation Test App -Demonstrates Windows implementation of the -[`file_selector` plugin](https://pub.dev/packages/file_selector). +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/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 b282b9030d67..0699dd121541 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 @@ -8,6 +8,9 @@ import 'package:flutter/material.dart'; /// Screen that allows the user to select a directory using `getDirectoryPath`, /// then displays the selected directory in a dialog. class GetDirectoryPage extends StatelessWidget { + /// Default Constructor + const GetDirectoryPage({Key? key}) : super(key: key); + Future _getDirectoryPath(BuildContext context) async { const String confirmButtonText = 'Choose'; final String? directoryPath = @@ -36,7 +39,10 @@ class GetDirectoryPage extends StatelessWidget { 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 a directory'), @@ -52,7 +58,7 @@ class GetDirectoryPage extends StatelessWidget { /// Widget that displays a text file in a dialog. class TextDisplay extends StatelessWidget { /// Creates a `TextDisplay`. - const TextDisplay(this.directoryPath); + const TextDisplay(this.directoryPath, {Key? key}) : super(key: key); /// The path selected in the dialog. final String directoryPath; diff --git a/packages/file_selector/file_selector_windows/example/lib/home_page.dart b/packages/file_selector/file_selector_windows/example/lib/home_page.dart index 958680be0e3b..a4b2ae1f63ea 100644 --- a/packages/file_selector/file_selector_windows/example/lib/home_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/home_page.dart @@ -6,10 +6,16 @@ import 'package:flutter/material.dart'; /// Home Page of the application. class HomePage extends StatelessWidget { + /// Default Constructor + const HomePage({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { final ButtonStyle 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, ); return Scaffold( diff --git a/packages/file_selector/file_selector_windows/example/lib/main.dart b/packages/file_selector/file_selector_windows/example/lib/main.dart index a49ebac1aea5..3e447104ef9f 100644 --- a/packages/file_selector/file_selector_windows/example/lib/main.dart +++ b/packages/file_selector/file_selector_windows/example/lib/main.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 'package:example/get_directory_page.dart'; -import 'package:example/home_page.dart'; -import 'package:example/open_image_page.dart'; -import 'package:example/open_multiple_images_page.dart'; -import 'package:example/open_text_page.dart'; -import 'package:example/save_text_page.dart'; import 'package:flutter/material.dart'; +import 'get_directory_page.dart'; +import 'home_page.dart'; +import 'open_image_page.dart'; +import 'open_multiple_images_page.dart'; +import 'open_text_page.dart'; +import 'save_text_page.dart'; + void main() { - runApp(MyApp()); + runApp(const MyApp()); } /// MyApp is the Main Application. class MyApp extends StatelessWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -24,13 +28,14 @@ class MyApp extends StatelessWidget { primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: HomePage(), + home: const HomePage(), routes: { - '/open/image': (BuildContext context) => OpenImagePage(), - '/open/images': (BuildContext context) => OpenMultipleImagesPage(), - '/open/text': (BuildContext context) => OpenTextPage(), + '/open/image': (BuildContext context) => const OpenImagePage(), + '/open/images': (BuildContext context) => + const OpenMultipleImagesPage(), + '/open/text': (BuildContext context) => const OpenTextPage(), '/save/text': (BuildContext context) => SaveTextPage(), - '/directory': (BuildContext context) => GetDirectoryPage(), + '/directory': (BuildContext context) => const GetDirectoryPage(), }, ); } 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 aaf083603e72..b6ada56ebb2b 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 @@ -11,8 +11,11 @@ import 'package:flutter/material.dart'; /// Screen that allows the user to select an image file using /// `openFiles`, then displays the selected images in a gallery dialog. class OpenImagePage extends StatelessWidget { + /// Default Constructor + const OpenImagePage({Key? key}) : super(key: key); + Future _openImageFile(BuildContext context) async { - final XTypeGroup typeGroup = XTypeGroup( + const XTypeGroup typeGroup = XTypeGroup( label: 'images', extensions: ['jpg', 'png'], ); @@ -43,7 +46,10 @@ class OpenImagePage extends StatelessWidget { 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 open an image file(png, jpg)'), @@ -59,7 +65,8 @@ class OpenImagePage extends StatelessWidget { /// Widget that displays an image in a dialog. class ImageDisplay extends StatelessWidget { /// Default Constructor. - const ImageDisplay(this.fileName, this.filePath); + const ImageDisplay(this.fileName, this.filePath, {Key? key}) + : super(key: key); /// The name of the selected file. final String fileName; 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 a030b8b4b10b..c8e352a5b8bd 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 @@ -11,12 +11,15 @@ import 'package:flutter/material.dart'; /// Screen that allows the user to select multiple image files using /// `openFiles`, then displays the selected images in a gallery dialog. class OpenMultipleImagesPage extends StatelessWidget { + /// Default Constructor + const OpenMultipleImagesPage({Key? key}) : super(key: key); + Future _openImageFile(BuildContext context) async { - final XTypeGroup jpgsTypeGroup = XTypeGroup( + const XTypeGroup jpgsTypeGroup = XTypeGroup( label: 'JPEGs', extensions: ['jpg', 'jpeg'], ); - final XTypeGroup pngTypeGroup = XTypeGroup( + const XTypeGroup pngTypeGroup = XTypeGroup( label: 'PNGs', extensions: ['png'], ); @@ -47,7 +50,10 @@ class OpenMultipleImagesPage extends StatelessWidget { 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 open multiple images (png, jpg)'), @@ -63,7 +69,7 @@ class OpenMultipleImagesPage extends StatelessWidget { /// Widget that displays a text file in a dialog. class MultipleImagesDisplay extends StatelessWidget { /// Default Constructor. - const MultipleImagesDisplay(this.files); + const MultipleImagesDisplay(this.files, {Key? key}) : super(key: key); /// The files containing the images. final List files; 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 fa281a0020d5..4c88d7475049 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 @@ -8,8 +8,11 @@ import 'package:flutter/material.dart'; /// Screen that allows the user to select a text file using `openFile`, then /// displays its contents in a dialog. class OpenTextPage extends StatelessWidget { + /// Default Constructor + const OpenTextPage({Key? key}) : super(key: key); + Future _openTextFile(BuildContext context) async { - final XTypeGroup typeGroup = XTypeGroup( + const XTypeGroup typeGroup = XTypeGroup( label: 'text', extensions: ['txt', 'json'], ); @@ -40,7 +43,10 @@ class OpenTextPage extends StatelessWidget { 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 open a text file (json, txt)'), @@ -56,7 +62,8 @@ class OpenTextPage extends StatelessWidget { /// Widget that displays a text file in a dialog. class TextDisplay extends StatelessWidget { /// Default Constructor. - const TextDisplay(this.fileName, this.fileContent); + const TextDisplay(this.fileName, this.fileContent, {Key? key}) + : super(key: key); /// The name of the selected file. final String fileName; diff --git a/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart b/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart index b87a51c3877d..9803f285a536 100644 --- a/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart @@ -9,6 +9,9 @@ import 'package:flutter/material.dart'; /// Screen that allows the user to select a save location using `getSavePath`, /// then writes text to a file at that location. class SaveTextPage extends StatelessWidget { + /// Default Constructor + SaveTextPage({Key? key}) : super(key: key); + final TextEditingController _nameController = TextEditingController(); final TextEditingController _contentController = TextEditingController(); @@ -64,11 +67,14 @@ class SaveTextPage extends StatelessWidget { const SizedBox(height: 10), 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 save a text file'), onPressed: _saveFile, + child: const Text('Press to save a text file'), ), ], ), diff --git a/packages/file_selector/file_selector_windows/example/pubspec.yaml b/packages/file_selector/file_selector_windows/example/pubspec.yaml index b66a2023deb2..bc886d32c896 100644 --- a/packages/file_selector/file_selector_windows/example/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/example/pubspec.yaml @@ -5,10 +5,10 @@ version: 1.0.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" dependencies: - file_selector_platform_interface: ^2.0.0 + file_selector_platform_interface: ^2.2.0 file_selector_windows: # When depending on this package from a real application you should use: # file_selector_windows: ^x.y.z diff --git a/packages/file_selector/file_selector_windows/example/windows/flutter/generated_plugins.cmake b/packages/file_selector/file_selector_windows/example/windows/flutter/generated_plugins.cmake index 63eda9b7b59f..a423a02476a2 100644 --- a/packages/file_selector/file_selector_windows/example/windows/flutter/generated_plugins.cmake +++ b/packages/file_selector/file_selector_windows/example/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart b/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart index a8b159711e2a..4ce248343abb 100644 --- a/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart +++ b/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart @@ -2,19 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:cross_file/cross_file.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; -import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; -const MethodChannel _channel = - MethodChannel('plugins.flutter.io/file_selector_windows'); +import 'src/messages.g.dart'; /// An implementation of [FileSelectorPlatform] for Windows. class FileSelectorWindows extends FileSelectorPlatform { - /// The MethodChannel that is being used by this implementation of the plugin. - @visibleForTesting - MethodChannel get channel => _channel; + final FileSelectorApi _hostApi = FileSelectorApi(); /// Registers the Windows implementation. static void registerWith() { @@ -27,18 +21,15 @@ class FileSelectorWindows extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List? path = await _channel.invokeListMethod( - 'openFile', - { - 'acceptedTypeGroups': acceptedTypeGroups - ?.map((XTypeGroup group) => group.toJSON()) - .toList(), - 'initialDirectory': initialDirectory, - 'confirmButtonText': confirmButtonText, - 'multiple': false, - }, - ); - return path == null ? null : XFile(path.first); + final List paths = await _hostApi.showOpenDialog( + SelectionOptions( + allowMultiple: false, + selectFolders: false, + allowedTypes: _typeGroupsFromXTypeGroups(acceptedTypeGroups), + ), + initialDirectory, + confirmButtonText); + return paths.isEmpty ? null : XFile(paths.first!); } @override @@ -47,18 +38,15 @@ class FileSelectorWindows extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List? pathList = await _channel.invokeListMethod( - 'openFile', - { - 'acceptedTypeGroups': acceptedTypeGroups - ?.map((XTypeGroup group) => group.toJSON()) - .toList(), - 'initialDirectory': initialDirectory, - 'confirmButtonText': confirmButtonText, - 'multiple': true, - }, - ); - return pathList?.map((String path) => XFile(path)).toList() ?? []; + final List paths = await _hostApi.showOpenDialog( + SelectionOptions( + allowMultiple: true, + selectFolders: false, + allowedTypes: _typeGroupsFromXTypeGroups(acceptedTypeGroups), + ), + initialDirectory, + confirmButtonText); + return paths.map((String? path) => XFile(path!)).toList(); } @override @@ -68,17 +56,16 @@ class FileSelectorWindows extends FileSelectorPlatform { String? suggestedName, String? confirmButtonText, }) async { - return _channel.invokeMethod( - 'getSavePath', - { - 'acceptedTypeGroups': acceptedTypeGroups - ?.map((XTypeGroup group) => group.toJSON()) - .toList(), - 'initialDirectory': initialDirectory, - 'suggestedName': suggestedName, - 'confirmButtonText': confirmButtonText, - }, - ); + final List paths = await _hostApi.showSaveDialog( + SelectionOptions( + allowMultiple: false, + selectFolders: false, + allowedTypes: _typeGroupsFromXTypeGroups(acceptedTypeGroups), + ), + initialDirectory, + suggestedName, + confirmButtonText); + return paths.isEmpty ? null : paths.first!; } @override @@ -86,12 +73,27 @@ class FileSelectorWindows extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - return _channel.invokeMethod( - 'getDirectoryPath', - { - 'initialDirectory': initialDirectory, - 'confirmButtonText': confirmButtonText, - }, - ); + final List paths = await _hostApi.showOpenDialog( + SelectionOptions( + allowMultiple: false, + selectFolders: true, + allowedTypes: [], + ), + initialDirectory, + confirmButtonText); + return paths.isEmpty ? null : paths.first!; } } + +List _typeGroupsFromXTypeGroups(List? xtypes) { + return (xtypes ?? []).map((XTypeGroup xtype) { + if (!xtype.allowsAny && (xtype.extensions?.isEmpty ?? true)) { + throw ArgumentError('Provided type group $xtype does not allow ' + 'all files, but does not set any of the Windows-supported filter ' + 'categories. "extensions" must be non-empty for Windows if ' + 'anything is non-empty.'); + } + return TypeGroup( + label: xtype.label ?? '', extensions: xtype.extensions ?? []); + }).toList(); +} diff --git a/packages/file_selector/file_selector_windows/lib/src/messages.g.dart b/packages/file_selector/file_selector_windows/lib/src/messages.g.dart new file mode 100644 index 000000000000..ad3d5af83278 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/messages.g.dart @@ -0,0 +1,176 @@ +// 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.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +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 TypeGroup { + TypeGroup({ + required this.label, + required this.extensions, + }); + + String label; + List extensions; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['label'] = label; + pigeonMap['extensions'] = extensions; + return pigeonMap; + } + + static TypeGroup decode(Object message) { + final Map pigeonMap = message as Map; + return TypeGroup( + label: pigeonMap['label']! as String, + extensions: (pigeonMap['extensions'] as List?)!.cast(), + ); + } +} + +class SelectionOptions { + SelectionOptions({ + required this.allowMultiple, + required this.selectFolders, + required this.allowedTypes, + }); + + bool allowMultiple; + bool selectFolders; + List allowedTypes; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['allowMultiple'] = allowMultiple; + pigeonMap['selectFolders'] = selectFolders; + pigeonMap['allowedTypes'] = allowedTypes; + return pigeonMap; + } + + static SelectionOptions decode(Object message) { + final Map pigeonMap = message as Map; + return SelectionOptions( + allowMultiple: pigeonMap['allowMultiple']! as bool, + selectFolders: pigeonMap['selectFolders']! as bool, + allowedTypes: + (pigeonMap['allowedTypes'] as List?)!.cast(), + ); + } +} + +class _FileSelectorApiCodec extends StandardMessageCodec { + const _FileSelectorApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is SelectionOptions) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is TypeGroup) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return SelectionOptions.decode(readValue(buffer)!); + + case 129: + return TypeGroup.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class FileSelectorApi { + /// Constructor for [FileSelectorApi]. 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. + FileSelectorApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _FileSelectorApiCodec(); + + Future> showOpenDialog(SelectionOptions arg_options, + String? arg_initialDirectory, String? arg_confirmButtonText) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.showOpenDialog', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel.send( + [arg_options, arg_initialDirectory, arg_confirmButtonText]) + 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> showSaveDialog( + SelectionOptions arg_options, + String? arg_initialDirectory, + String? arg_suggestedName, + String? arg_confirmButtonText) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.showSaveDialog', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel.send([ + arg_options, + arg_initialDirectory, + arg_suggestedName, + arg_confirmButtonText + ]) 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(); + } + } +} diff --git a/packages/file_selector/file_selector_windows/pigeons/copyright.txt b/packages/file_selector/file_selector_windows/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/file_selector/file_selector_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/file_selector/file_selector_windows/pigeons/messages.dart b/packages/file_selector/file_selector_windows/pigeons/messages.dart new file mode 100644 index 000000000000..f2c9ab71bd82 --- /dev/null +++ b/packages/file_selector/file_selector_windows/pigeons/messages.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:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + dartTestOut: 'test/test_api.dart', + cppOptions: CppOptions(namespace: 'file_selector_windows'), + cppHeaderOut: 'windows/messages.g.h', + cppSourceOut: 'windows/messages.g.cpp', + copyrightHeader: 'pigeons/copyright.txt', +)) +class TypeGroup { + TypeGroup(this.label, {required this.extensions}); + + String label; + // TODO(stuartmorgan): Make the generic type non-nullable once supported. + // https://github.com/flutter/flutter/issues/97848 + // The C++ code treats all of it as non-nullable. + List extensions; +} + +class SelectionOptions { + SelectionOptions({ + this.allowMultiple = false, + this.selectFolders = false, + this.allowedTypes = const [], + }); + bool allowMultiple; + bool selectFolders; + + // TODO(stuartmorgan): Make the generic type non-nullable once supported. + // https://github.com/flutter/flutter/issues/97848 + // The C++ code treats the values as non-nullable. + List allowedTypes; +} + +@HostApi(dartHostTestHandler: 'TestFileSelectorApi') +abstract class FileSelectorApi { + List showOpenDialog( + SelectionOptions options, + String? initialDirectory, + String? confirmButtonText, + ); + List showSaveDialog( + SelectionOptions options, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + ); +} diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml index 7b035e974293..ee0701b3fd30 100644 --- a/packages/file_selector/file_selector_windows/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/pubspec.yaml @@ -1,12 +1,12 @@ name: file_selector_windows description: Windows implementation of the file_selector plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector_windows +repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.8.2 +version: 0.9.1+4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: @@ -18,8 +18,13 @@ flutter: dependencies: cross_file: ^0.3.1 - file_selector_platform_interface: ^2.0.4 + file_selector_platform_interface: ^2.2.0 flutter: sdk: flutter + +dev_dependencies: + build_runner: 2.1.11 flutter_test: sdk: flutter + mockito: ^5.1.0 + pigeon: ^3.2.5 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 72604dd1668c..f07c9b67618d 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 @@ -4,254 +4,321 @@ import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_windows/file_selector_windows.dart'; -import 'package:flutter/services.dart'; +import 'package:file_selector_windows/src/messages.g.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'file_selector_windows_test.mocks.dart'; +import 'test_api.dart'; + +@GenerateMocks([TestFileSelectorApi]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('$FileSelectorWindows()', () { - final FileSelectorWindows plugin = FileSelectorWindows(); + final FileSelectorWindows plugin = FileSelectorWindows(); + late MockTestFileSelectorApi mockApi; + + setUp(() { + mockApi = MockTestFileSelectorApi(); + TestFileSelectorApi.setup(mockApi); + }); + + test('registered instance', () { + FileSelectorWindows.registerWith(); + expect(FileSelectorPlatform.instance, isA()); + }); + + group('#openFile', () { + setUp(() { + when(mockApi.showOpenDialog(any, any, any)).thenReturn(['foo']); + }); + + test('simple call works', () async { + final XFile? file = await plugin.openFile(); + + expect(file!.path, 'foo'); + final VerificationResult result = + verify(mockApi.showOpenDialog(captureAny, null, null)); + final SelectionOptions options = result.captured[0] as SelectionOptions; + expect(options.allowMultiple, false); + expect(options.selectFolders, false); + }); + + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image']); + + await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); + + final VerificationResult result = + verify(mockApi.showOpenDialog(captureAny, null, null)); + final SelectionOptions options = result.captured[0] as SelectionOptions; + expect( + _typeGroupListsMatch(options.allowedTypes, [ + TypeGroup(label: 'text', extensions: ['txt']), + TypeGroup(label: 'image', extensions: ['jpg']), + ]), + true); + }); + + test('passes initialDirectory correctly', () async { + await plugin.openFile(initialDirectory: '/example/directory'); + + verify(mockApi.showOpenDialog(any, '/example/directory', null)); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.openFile(confirmButtonText: 'Open File'); + + verify(mockApi.showOpenDialog(any, null, 'Open File')); + }); + + test('throws for a type group that does not support Windows', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + mimeTypes: ['text/plain'], + ); + + await expectLater( + plugin.openFile(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('allows a wildcard group', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + ); + + await expectLater( + plugin.openFile(acceptedTypeGroups: [group]), completes); + }); + }); + + group('#openFiles', () { + setUp(() { + when(mockApi.showOpenDialog(any, any, any)) + .thenReturn(['foo', 'bar']); + }); + + test('simple call works', () async { + final List file = await plugin.openFiles(); + + expect(file[0].path, 'foo'); + expect(file[1].path, 'bar'); + final VerificationResult result = + verify(mockApi.showOpenDialog(captureAny, null, null)); + final SelectionOptions options = result.captured[0] as SelectionOptions; + expect(options.allowMultiple, true); + expect(options.selectFolders, false); + }); + + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image']); + + await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); + + final VerificationResult result = + verify(mockApi.showOpenDialog(captureAny, null, null)); + final SelectionOptions options = result.captured[0] as SelectionOptions; + expect( + _typeGroupListsMatch(options.allowedTypes, [ + TypeGroup(label: 'text', extensions: ['txt']), + TypeGroup(label: 'image', extensions: ['jpg']), + ]), + true); + }); + + test('passes initialDirectory correctly', () async { + await plugin.openFiles(initialDirectory: '/example/directory'); + + verify(mockApi.showOpenDialog(any, '/example/directory', null)); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.openFiles(confirmButtonText: 'Open Files'); + + verify(mockApi.showOpenDialog(any, null, 'Open Files')); + }); + + test('throws for a type group that does not support Windows', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + mimeTypes: ['text/plain'], + ); + + await expectLater( + plugin.openFiles(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('allows a wildcard group', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + ); + + await expectLater( + plugin.openFiles(acceptedTypeGroups: [group]), completes); + }); + }); + + group('#getDirectoryPath', () { + setUp(() { + when(mockApi.showOpenDialog(any, any, any)).thenReturn(['foo']); + }); + + test('simple call works', () async { + final String? path = await plugin.getDirectoryPath(); + + expect(path, 'foo'); + final VerificationResult result = + verify(mockApi.showOpenDialog(captureAny, null, null)); + final SelectionOptions options = result.captured[0] as SelectionOptions; + expect(options.allowMultiple, false); + expect(options.selectFolders, true); + }); + + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPath(initialDirectory: '/example/directory'); + + verify(mockApi.showOpenDialog(any, '/example/directory', null)); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPath(confirmButtonText: 'Open Directory'); - final List log = []; + verify(mockApi.showOpenDialog(any, null, 'Open Directory')); + }); + }); + group('#getSavePath', () { setUp(() { - plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return null; - }); + when(mockApi.showSaveDialog(any, any, any, any)) + .thenReturn(['foo']); + }); - log.clear(); + test('simple call works', () async { + final String? path = await plugin.getSavePath(); + + expect(path, 'foo'); + final VerificationResult result = + verify(mockApi.showSaveDialog(captureAny, null, null, null)); + final SelectionOptions options = result.captured[0] as SelectionOptions; + expect(options.allowMultiple, false); + expect(options.selectFolders, false); }); - test('registered instance', () { - FileSelectorWindows.registerWith(); - expect(FileSelectorPlatform.instance, isA()); + test('passes the accepted type groups correctly', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + const XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image']); + + await plugin + .getSavePath(acceptedTypeGroups: [group, groupTwo]); + + final VerificationResult result = + verify(mockApi.showSaveDialog(captureAny, null, null, null)); + final SelectionOptions options = result.captured[0] as SelectionOptions; + expect( + _typeGroupListsMatch(options.allowedTypes, [ + TypeGroup(label: 'text', extensions: ['txt']), + TypeGroup(label: 'image', extensions: ['jpg']), + ]), + true); }); - group('#openFile', () { - test('passes the accepted type groups correctly', () async { - final XTypeGroup group = XTypeGroup( - label: 'text', - extensions: ['txt'], - mimeTypes: ['text/plain'], - macUTIs: ['public.text'], - ); - - final XTypeGroup groupTwo = XTypeGroup( - label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - webWildCards: ['image/*']); - - await plugin - .openFile(acceptedTypeGroups: [group, groupTwo]); - - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - group.toJSON(), - groupTwo.toJSON() - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }), - ], - ); - }); - test('passes initialDirectory correctly', () async { - await plugin.openFile(initialDirectory: '/example/directory'); - - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': null, - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': false, - }), - ], - ); - }); - test('passes confirmButtonText correctly', () async { - await plugin.openFile(confirmButtonText: 'Open File'); - - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': null, - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': false, - }), - ], - ); - }); + test('passes initialDirectory correctly', () async { + await plugin.getSavePath(initialDirectory: '/example/directory'); + + verify(mockApi.showSaveDialog(any, '/example/directory', null, null)); }); - group('#openFiles', () { - test('passes the accepted type groups correctly', () async { - final XTypeGroup group = XTypeGroup( - label: 'text', - extensions: ['txt'], - mimeTypes: ['text/plain'], - macUTIs: ['public.text'], - ); - - final XTypeGroup groupTwo = XTypeGroup( - label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - webWildCards: ['image/*']); - - await plugin - .openFiles(acceptedTypeGroups: [group, groupTwo]); - - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - group.toJSON(), - groupTwo.toJSON() - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': true, - }), - ], - ); - }); - test('passes initialDirectory correctly', () async { - await plugin.openFiles(initialDirectory: '/example/directory'); - - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': null, - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': true, - }), - ], - ); - }); - test('passes confirmButtonText correctly', () async { - await plugin.openFiles(confirmButtonText: 'Open File'); - - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': null, - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': true, - }), - ], - ); - }); + + test('passes suggestedName correctly', () async { + await plugin.getSavePath(suggestedName: 'baz.txt'); + + verify(mockApi.showSaveDialog(any, null, 'baz.txt', null)); }); - group('#getSavePath', () { - test('passes the accepted type groups correctly', () async { - final XTypeGroup group = XTypeGroup( - label: 'text', - extensions: ['txt'], - mimeTypes: ['text/plain'], - macUTIs: ['public.text'], - ); - - final XTypeGroup groupTwo = XTypeGroup( - label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - webWildCards: ['image/*']); - - await plugin - .getSavePath(acceptedTypeGroups: [group, groupTwo]); - - expect( - log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypeGroups': >[ - group.toJSON(), - groupTwo.toJSON() - ], - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], - ); - }); - test('passes initialDirectory correctly', () async { - await plugin.getSavePath(initialDirectory: '/example/directory'); - - expect( - log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypeGroups': null, - 'initialDirectory': '/example/directory', - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], - ); - }); - test('passes confirmButtonText correctly', () async { - await plugin.getSavePath(confirmButtonText: 'Open File'); - - expect( - log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypeGroups': null, - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': 'Open File', - }), - ], - ); - }); - group('#getDirectoryPath', () { - test('passes initialDirectory correctly', () async { - await plugin.getDirectoryPath(initialDirectory: '/example/directory'); - - expect( - log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - }), - ], - ); - }); - test('passes confirmButtonText correctly', () async { - await plugin.getDirectoryPath(confirmButtonText: 'Open File'); - - expect( - log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - }), - ], - ); - }); - }); + test('passes confirmButtonText correctly', () async { + await plugin.getSavePath(confirmButtonText: 'Save File'); + + verify(mockApi.showSaveDialog(any, null, null, 'Save File')); + }); + + test('throws for a type group that does not support Windows', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + mimeTypes: ['text/plain'], + ); + + await expectLater( + plugin.getSavePath(acceptedTypeGroups: [group]), + throwsArgumentError); + }); + + test('allows a wildcard group', () async { + const XTypeGroup group = XTypeGroup( + label: 'text', + ); + + await expectLater( + plugin.getSavePath(acceptedTypeGroups: [group]), + completes); }); }); } + +// True if the given options match. +// +// This is needed because Pigeon data classes don't have custom equality checks, +// so only match for identical instances. +bool _typeGroupListsMatch(List a, List b) { + if (a.length != b.length) { + return false; + } + for (int i = 0; i < a.length; i++) { + if (!_typeGroupsMatch(a[i], b[i])) { + return false; + } + } + return true; +} + +// True if the given type groups match. +// +// This is needed because Pigeon data classes don't have custom equality checks, +// so only match for identical instances. +bool _typeGroupsMatch(TypeGroup? a, TypeGroup? b) { + return a!.label == b!.label && listEquals(a.extensions, b.extensions); +} 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 new file mode 100644 index 000000000000..61e17fcdfeaa --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart @@ -0,0 +1,46 @@ +// Mocks generated by Mockito 5.2.0 from annotations +// in file_selector_windows/example/windows/flutter/ephemeral/.plugin_symlinks/file_selector_windows/test/file_selector_windows_test.dart. +// Do not manually edit this file. + +import 'package:file_selector_windows/src/messages.g.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_api.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 + +/// A class which mocks [TestFileSelectorApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestFileSelectorApi extends _i1.Mock + implements _i2.TestFileSelectorApi { + MockTestFileSelectorApi() { + _i1.throwOnMissingStub(this); + } + + @override + List showOpenDialog(_i3.SelectionOptions? options, + String? initialDirectory, String? confirmButtonText) => + (super.noSuchMethod( + Invocation.method( + #showOpenDialog, [options, initialDirectory, confirmButtonText]), + returnValue: []) as List); + @override + List showSaveDialog( + _i3.SelectionOptions? options, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText) => + (super.noSuchMethod( + Invocation.method(#showSaveDialog, + [options, initialDirectory, suggestedName, confirmButtonText]), + returnValue: []) as List); +} diff --git a/packages/file_selector/file_selector_windows/test/test_api.dart b/packages/file_selector/file_selector_windows/test/test_api.dart new file mode 100644 index 000000000000..f9b979f7b854 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/test_api.dart @@ -0,0 +1,105 @@ +// 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.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import +// ignore_for_file: avoid_relative_lib_imports +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'; + +// ignore: directives_ordering +import 'package:file_selector_windows/src/messages.g.dart'; + +class _TestFileSelectorApiCodec extends StandardMessageCodec { + const _TestFileSelectorApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is SelectionOptions) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is TypeGroup) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return SelectionOptions.decode(readValue(buffer)!); + + case 129: + return TypeGroup.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestFileSelectorApi { + static const MessageCodec codec = _TestFileSelectorApiCodec(); + + List showOpenDialog(SelectionOptions options, + String? initialDirectory, String? confirmButtonText); + List showSaveDialog( + SelectionOptions options, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText); + static void setup(TestFileSelectorApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.showOpenDialog', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.showOpenDialog was null.'); + final List args = (message as List?)!; + final SelectionOptions? arg_options = (args[0] as SelectionOptions?); + assert(arg_options != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.showOpenDialog was null, expected non-null SelectionOptions.'); + final String? arg_initialDirectory = (args[1] as String?); + final String? arg_confirmButtonText = (args[2] as String?); + final List output = api.showOpenDialog( + arg_options!, arg_initialDirectory, arg_confirmButtonText); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.showSaveDialog', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.showSaveDialog was null.'); + final List args = (message as List?)!; + final SelectionOptions? arg_options = (args[0] as SelectionOptions?); + assert(arg_options != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.showSaveDialog was null, expected non-null SelectionOptions.'); + final String? arg_initialDirectory = (args[1] as String?); + final String? arg_suggestedName = (args[2] as String?); + final String? arg_confirmButtonText = (args[3] as String?); + final List output = api.showSaveDialog(arg_options!, + arg_initialDirectory, arg_suggestedName, arg_confirmButtonText); + return {'result': output}; + }); + } + } + } +} diff --git a/packages/file_selector/file_selector_windows/windows/CMakeLists.txt b/packages/file_selector/file_selector_windows/windows/CMakeLists.txt index 01c9f58b98a2..e06f3749e0f7 100644 --- a/packages/file_selector/file_selector_windows/windows/CMakeLists.txt +++ b/packages/file_selector/file_selector_windows/windows/CMakeLists.txt @@ -9,6 +9,8 @@ list(APPEND PLUGIN_SOURCES "file_dialog_controller.h" "file_selector_plugin.cpp" "file_selector_plugin.h" + "messages.g.cpp" + "messages.g.h" "string_utils.cpp" "string_utils.h" ) @@ -24,6 +26,9 @@ target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) +# Override apply_standard_settings for exceptions due to +# https://developercommunity.visualstudio.com/t/stdany-doesnt-link-when-exceptions-are-disabled/376072 +target_compile_definitions(${PLUGIN_NAME} PRIVATE "_HAS_EXCEPTIONS=1") # List of absolute paths to libraries that should be bundled with the plugin set(file_selector_bundled_libraries @@ -67,6 +72,9 @@ apply_standard_settings(${TEST_RUNNER}) target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin) target_link_libraries(${TEST_RUNNER} PRIVATE gtest gmock) +# Override apply_standard_settings for exceptions due to +# https://developercommunity.visualstudio.com/t/stdany-doesnt-link-when-exceptions-are-disabled/376072 +target_compile_definitions(${TEST_RUNNER} PRIVATE "_HAS_EXCEPTIONS=1") # flutter_wrapper_plugin has link dependencies on the Flutter DLL. add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different diff --git a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp index e4b1a2afcdde..5820c4a5da40 100644 --- a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp +++ b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp @@ -17,8 +17,8 @@ FileDialogController::FileDialogController(IFileDialog* dialog) FileDialogController::~FileDialogController() {} -HRESULT FileDialogController::SetDefaultFolder(IShellItem* folder) { - return dialog_->SetDefaultFolder(folder); +HRESULT FileDialogController::SetFolder(IShellItem* folder) { + return dialog_->SetFolder(folder); } HRESULT FileDialogController::SetFileName(const wchar_t* name) { diff --git a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h index e7357338243e..f5c93974cbe9 100644 --- a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h +++ b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h @@ -30,7 +30,7 @@ class FileDialogController { FileDialogController& operator=(const FileDialogController&) = delete; // IFileDialog wrappers: - virtual HRESULT SetDefaultFolder(IShellItem* folder); + virtual HRESULT SetFolder(IShellItem* folder); virtual HRESULT SetFileName(const wchar_t* name); virtual HRESULT SetFileTypes(UINT count, COMDLG_FILTERSPEC* filters); virtual HRESULT SetOkButtonLabel(const wchar_t* text); diff --git a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp index 870bc281b6f6..b9e6d211b2d1 100644 --- a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp +++ b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp @@ -33,33 +33,8 @@ using flutter::EncodableList; using flutter::EncodableMap; using flutter::EncodableValue; -// From file_selector_windows.dart -constexpr char kChannelName[] = "plugins.flutter.io/file_selector_windows"; - -constexpr char kOpenFileMethod[] = "openFile"; -constexpr char kGetSavePathMethod[] = "getSavePath"; -constexpr char kGetDirectoryPathMethod[] = "getDirectoryPath"; - -constexpr char kAcceptedTypeGroupsKey[] = "acceptedTypeGroups"; -constexpr char kConfirmButtonTextKey[] = "confirmButtonText"; -constexpr char kInitialDirectoryKey[] = "initialDirectory"; -constexpr char kMultipleKey[] = "multiple"; -constexpr char kSuggestedNameKey[] = "suggestedName"; - -// From x_type_group.dart -// Only 'extensions' are supported by Windows for filtering. -constexpr char kTypeGroupLabelKey[] = "label"; -constexpr char kTypeGroupExtensionsKey[] = "extensions"; - -// Looks for |key| in |map|, returning the associated value if it is present, or -// a nullptr if not. -const EncodableValue* ValueOrNull(const EncodableMap& map, const char* key) { - auto it = map.find(EncodableValue(key)); - if (it == map.end()) { - return nullptr; - } - return &(it->second); -} +// The kind of file dialog to show. +enum class DialogMode { open, save }; // Returns the path for |shell_item| as a UTF-8 string, or an // empty string on failure. @@ -111,7 +86,7 @@ class DialogWrapper { // Attempts to set the default folder for the dialog to |path|, // if it exists. - void SetDefaultFolder(const std::string& path) { + void SetFolder(std::string_view path) { std::wstring wide_path = Utf16FromUtf8(path); IShellItemPtr item; last_result_ = SHCreateItemFromParsingName(wide_path.c_str(), nullptr, @@ -119,17 +94,17 @@ class DialogWrapper { if (!SUCCEEDED(last_result_)) { return; } - dialog_controller_->SetDefaultFolder(item); + dialog_controller_->SetFolder(item); } // Sets the file name that is initially shown in the dialog. - void SetFileName(const std::string& name) { + void SetFileName(std::string_view name) { std::wstring wide_name = Utf16FromUtf8(name); last_result_ = dialog_controller_->SetFileName(wide_name.c_str()); } // Sets the label of the confirmation button. - void SetOkButtonLabel(const std::string& label) { + void SetOkButtonLabel(std::string_view label) { std::wstring wide_label = Utf16FromUtf8(label); last_result_ = dialog_controller_->SetOkButtonLabel(wide_label.c_str()); } @@ -161,18 +136,15 @@ class DialogWrapper { filter_names.reserve(filters.size()); for (const EncodableValue& filter_info_value : filters) { - const auto& filter_info = std::get(filter_info_value); - const auto* filter_name = std::get_if( - ValueOrNull(filter_info, kTypeGroupLabelKey)); - const auto* extensions = std::get_if( - ValueOrNull(filter_info, kTypeGroupExtensionsKey)); - filter_names.push_back(filter_name ? Utf16FromUtf8(*filter_name) : L""); + const auto& type_group = std::any_cast( + std::get(filter_info_value)); + filter_names.push_back(Utf16FromUtf8(type_group.label())); filter_extensions.push_back(L""); std::wstring& spec = filter_extensions.back(); - if (!extensions || extensions->empty()) { + if (type_group.extensions().empty()) { spec += L"*.*"; } else { - for (const EncodableValue& extension : *extensions) { + for (const EncodableValue& extension : type_group.extensions()) { if (!spec.empty()) { spec += spec_delimiter; } @@ -186,50 +158,39 @@ class DialogWrapper { static_cast(filter_specs.size()), filter_specs.data()); } - // Displays the dialog, and returns the selected file or files as an - // EncodableValue of type List (for open) or String (for save), or a null - // EncodableValue on cancel or error. - EncodableValue Show(HWND parent_window) { + // Displays the dialog, and returns the selected files, or nullopt on error. + std::optional Show(HWND parent_window) { assert(dialog_controller_); last_result_ = dialog_controller_->Show(parent_window); if (!SUCCEEDED(last_result_)) { - return EncodableValue(); + return std::nullopt; } + EncodableList files; if (is_open_dialog_) { IShellItemArrayPtr shell_items; last_result_ = dialog_controller_->GetResults(&shell_items); if (!SUCCEEDED(last_result_)) { - return EncodableValue(); + return std::nullopt; } IEnumShellItemsPtr item_enumerator; last_result_ = shell_items->EnumItems(&item_enumerator); if (!SUCCEEDED(last_result_)) { - return EncodableValue(); + return std::nullopt; } - EncodableList files; IShellItemPtr shell_item; while (item_enumerator->Next(1, &shell_item, nullptr) == S_OK) { files.push_back(EncodableValue(GetPathForShellItem(shell_item))); } - if (opening_directory_) { - // The directory option expects a String, not a List. - if (files.empty()) { - return EncodableValue(); - } - return EncodableValue(files[0]); - } else { - return EncodableValue(std::move(files)); - } } else { IShellItemPtr shell_item; last_result_ = dialog_controller_->GetResult(&shell_item); if (!SUCCEEDED(last_result_)) { - return EncodableValue(); + return std::nullopt; } - EncodableValue file(GetPathForShellItem(shell_item)); - return file; + files.push_back(EncodableValue(GetPathForShellItem(shell_item))); } + return files; } // Returns the result of the last Win32 API call related to this object. @@ -244,67 +205,54 @@ class DialogWrapper { HRESULT last_result_; }; -// Displays the open or save dialog (according to |method|) and sends the -// selected file path(s) back to the engine via |result|, or sends an -// error on failure. -// -// |result| is guaranteed to be resolved by this function. -void ShowDialog(const FileDialogControllerFactory& dialog_factory, - HWND parent_window, const std::string& method, - const EncodableMap& args, - std::unique_ptr> result) { - IID dialog_type = method.compare(kGetSavePathMethod) == 0 - ? CLSID_FileSaveDialog - : CLSID_FileOpenDialog; +ErrorOr ShowDialog( + const FileDialogControllerFactory& dialog_factory, HWND parent_window, + DialogMode mode, const SelectionOptions& options, + const std::string* initial_directory, const std::string* suggested_name, + const std::string* confirm_label) { + IID dialog_type = + mode == DialogMode::save ? CLSID_FileSaveDialog : CLSID_FileOpenDialog; DialogWrapper dialog(dialog_factory, dialog_type); if (!SUCCEEDED(dialog.last_result())) { - result->Error("System error", "Could not create dialog", - EncodableValue(dialog.last_result())); - return; + return FlutterError("System error", "Could not create dialog", + EncodableValue(dialog.last_result())); } FILEOPENDIALOGOPTIONS dialog_options = 0; - if (method.compare(kGetDirectoryPathMethod) == 0) { + if (options.select_folders()) { dialog_options |= FOS_PICKFOLDERS; } - const auto* allow_multiple_selection = - std::get_if(ValueOrNull(args, kMultipleKey)); - if (allow_multiple_selection && *allow_multiple_selection) { + if (options.allow_multiple()) { dialog_options |= FOS_ALLOWMULTISELECT; } if (dialog_options != 0) { dialog.AddOptions(dialog_options); } - const auto* initial_dir = - std::get_if(ValueOrNull(args, kInitialDirectoryKey)); - if (initial_dir) { - dialog.SetDefaultFolder(*initial_dir); + if (initial_directory) { + dialog.SetFolder(*initial_directory); } - const auto* suggested_name = - std::get_if(ValueOrNull(args, kSuggestedNameKey)); if (suggested_name) { dialog.SetFileName(*suggested_name); } - const auto* confirm_label = - std::get_if(ValueOrNull(args, kConfirmButtonTextKey)); if (confirm_label) { dialog.SetOkButtonLabel(*confirm_label); } - const auto* accepted_types = - std::get_if(ValueOrNull(args, kAcceptedTypeGroupsKey)); - if (accepted_types && !accepted_types->empty()) { - dialog.SetFileTypeFilters(*accepted_types); + + if (!options.allowed_types().empty()) { + dialog.SetFileTypeFilters(options.allowed_types()); } - EncodableValue files = dialog.Show(parent_window); - if (files.IsNull() && - dialog.last_result() != HRESULT_FROM_WIN32(ERROR_CANCELLED)) { - ; - result->Error("System error", "Could not show dialog", - EncodableValue(dialog.last_result())); + std::optional files = dialog.Show(parent_window); + if (!files) { + if (dialog.last_result() != HRESULT_FROM_WIN32(ERROR_CANCELLED)) { + return FlutterError("System error", "Could not show dialog", + EncodableValue(dialog.last_result())); + } else { + return EncodableList(); + } } - result->Success(files); + return std::move(files.value()); } // Returns the top-level window that owns |view|. @@ -317,20 +265,12 @@ HWND GetRootWindow(flutter::FlutterView* view) { // static void FileSelectorPlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows* registrar) { - auto channel = std::make_unique>( - registrar->messenger(), kChannelName, - &flutter::StandardMethodCodec::GetInstance()); - std::unique_ptr plugin = std::make_unique( [registrar] { return GetRootWindow(registrar->GetView()); }, std::make_unique()); - channel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto& call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - + FileSelectorApi::SetUp(registrar->messenger(), plugin.get()); registrar->AddPlugin(std::move(plugin)); } @@ -342,21 +282,19 @@ FileSelectorPlugin::FileSelectorPlugin( FileSelectorPlugin::~FileSelectorPlugin() = default; -void FileSelectorPlugin::HandleMethodCall( - const flutter::MethodCall<>& method_call, - std::unique_ptr> result) { - const std::string& method_name = method_call.method_name(); - if (method_name.compare(kOpenFileMethod) == 0 || - method_name.compare(kGetSavePathMethod) == 0 || - method_name.compare(kGetDirectoryPathMethod) == 0) { - const auto* arguments = - std::get_if(method_call.arguments()); - assert(arguments); - ShowDialog(*controller_factory_, get_root_window_(), method_name, - *arguments, std::move(result)); - } else { - result->NotImplemented(); - } +ErrorOr FileSelectorPlugin::ShowOpenDialog( + const SelectionOptions& options, const std::string* initialDirectory, + const std::string* confirmButtonText) { + return ShowDialog(*controller_factory_, get_root_window_(), DialogMode::open, + options, initialDirectory, nullptr, confirmButtonText); +} + +ErrorOr FileSelectorPlugin::ShowSaveDialog( + const SelectionOptions& options, const std::string* initialDirectory, + const std::string* suggestedName, const std::string* confirmButtonText) { + return ShowDialog(*controller_factory_, get_root_window_(), DialogMode::save, + options, initialDirectory, suggestedName, + confirmButtonText); } } // namespace file_selector_windows diff --git a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.h b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.h index 292d312bea30..1388bfd3898d 100644 --- a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.h +++ b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.h @@ -10,6 +10,7 @@ #include #include "file_dialog_controller.h" +#include "messages.g.h" namespace file_selector_windows { @@ -18,7 +19,7 @@ namespace file_selector_windows { // around https://github.com/flutter/flutter/issues/90694. using FlutterRootWindowProvider = std::function; -class FileSelectorPlugin : public flutter::Plugin { +class FileSelectorPlugin : public flutter::Plugin, public FileSelectorApi { public: static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); @@ -30,9 +31,14 @@ class FileSelectorPlugin : public flutter::Plugin { virtual ~FileSelectorPlugin(); - // Called when a method is called on plugin channel; - void HandleMethodCall(const flutter::MethodCall<>& method_call, - std::unique_ptr> result); + // FileSelectorApi + ErrorOr ShowOpenDialog( + const SelectionOptions& options, const std::string* initial_directory, + const std::string* confirm_button_text) override; + ErrorOr ShowSaveDialog( + const SelectionOptions& options, const std::string* initialDirectory, + const std::string* suggestedName, + const std::string* confirmButtonText) override; private: // The provider for the root window to attach the dialog to. diff --git a/packages/file_selector/file_selector_windows/windows/messages.g.cpp b/packages/file_selector/file_selector_windows/windows/messages.g.cpp new file mode 100644 index 000000000000..04e529d8b35a --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/messages.g.cpp @@ -0,0 +1,278 @@ +// 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.5), 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 file_selector_windows { + +/* TypeGroup */ + +const std::string& TypeGroup::label() const { return label_; } +void TypeGroup::set_label(std::string_view value_arg) { label_ = value_arg; } + +const flutter::EncodableList& TypeGroup::extensions() const { + return extensions_; +} +void TypeGroup::set_extensions(const flutter::EncodableList& value_arg) { + extensions_ = value_arg; +} + +flutter::EncodableMap TypeGroup::ToEncodableMap() const { + return flutter::EncodableMap{ + {flutter::EncodableValue("label"), flutter::EncodableValue(label_)}, + {flutter::EncodableValue("extensions"), + flutter::EncodableValue(extensions_)}, + }; +} + +TypeGroup::TypeGroup() {} + +TypeGroup::TypeGroup(flutter::EncodableMap map) { + auto& encodable_label = map.at(flutter::EncodableValue("label")); + if (const std::string* pointer_label = + std::get_if(&encodable_label)) { + label_ = *pointer_label; + } + auto& encodable_extensions = map.at(flutter::EncodableValue("extensions")); + if (const flutter::EncodableList* pointer_extensions = + std::get_if(&encodable_extensions)) { + extensions_ = *pointer_extensions; + } +} + +/* SelectionOptions */ + +bool SelectionOptions::allow_multiple() const { return allow_multiple_; } +void SelectionOptions::set_allow_multiple(bool value_arg) { + allow_multiple_ = value_arg; +} + +bool SelectionOptions::select_folders() const { return select_folders_; } +void SelectionOptions::set_select_folders(bool value_arg) { + select_folders_ = value_arg; +} + +const flutter::EncodableList& SelectionOptions::allowed_types() const { + return allowed_types_; +} +void SelectionOptions::set_allowed_types( + const flutter::EncodableList& value_arg) { + allowed_types_ = value_arg; +} + +flutter::EncodableMap SelectionOptions::ToEncodableMap() const { + return flutter::EncodableMap{ + {flutter::EncodableValue("allowMultiple"), + flutter::EncodableValue(allow_multiple_)}, + {flutter::EncodableValue("selectFolders"), + flutter::EncodableValue(select_folders_)}, + {flutter::EncodableValue("allowedTypes"), + flutter::EncodableValue(allowed_types_)}, + }; +} + +SelectionOptions::SelectionOptions() {} + +SelectionOptions::SelectionOptions(flutter::EncodableMap map) { + auto& encodable_allow_multiple = + map.at(flutter::EncodableValue("allowMultiple")); + if (const bool* pointer_allow_multiple = + std::get_if(&encodable_allow_multiple)) { + allow_multiple_ = *pointer_allow_multiple; + } + auto& encodable_select_folders = + map.at(flutter::EncodableValue("selectFolders")); + if (const bool* pointer_select_folders = + std::get_if(&encodable_select_folders)) { + select_folders_ = *pointer_select_folders; + } + auto& encodable_allowed_types = + map.at(flutter::EncodableValue("allowedTypes")); + if (const flutter::EncodableList* pointer_allowed_types = + std::get_if(&encodable_allowed_types)) { + allowed_types_ = *pointer_allowed_types; + } +} + +FileSelectorApiCodecSerializer::FileSelectorApiCodecSerializer() {} +flutter::EncodableValue FileSelectorApiCodecSerializer::ReadValueOfType( + uint8_t type, flutter::ByteStreamReader* stream) const { + switch (type) { + case 128: + return flutter::CustomEncodableValue( + SelectionOptions(std::get(ReadValue(stream)))); + + case 129: + return flutter::CustomEncodableValue( + TypeGroup(std::get(ReadValue(stream)))); + + default: + return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + } +} + +void FileSelectorApiCodecSerializer::WriteValue( + const flutter::EncodableValue& value, + flutter::ByteStreamWriter* stream) const { + if (const flutter::CustomEncodableValue* custom_value = + std::get_if(&value)) { + if (custom_value->type() == typeid(SelectionOptions)) { + stream->WriteByte(128); + WriteValue( + std::any_cast(*custom_value).ToEncodableMap(), + stream); + return; + } + if (custom_value->type() == typeid(TypeGroup)) { + stream->WriteByte(129); + WriteValue(std::any_cast(*custom_value).ToEncodableMap(), + stream); + return; + } + } + flutter::StandardCodecSerializer::WriteValue(value, stream); +} + +/** The codec used by FileSelectorApi. */ +const flutter::StandardMessageCodec& FileSelectorApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance( + &FileSelectorApiCodecSerializer::GetInstance()); +} + +/** Sets up an instance of `FileSelectorApi` to handle messages through the + * `binary_messenger`. */ +void FileSelectorApi::SetUp(flutter::BinaryMessenger* binary_messenger, + FileSelectorApi* api) { + { + auto channel = + std::make_unique>( + binary_messenger, + "dev.flutter.pigeon.FileSelectorApi.showOpenDialog", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::EncodableValue& message, + const flutter::MessageReply& reply) { + flutter::EncodableMap wrapped; + try { + const auto& args = std::get(message); + const auto& encodable_options_arg = args.at(0); + if (encodable_options_arg.IsNull()) { + wrapped.emplace(flutter::EncodableValue("error"), + WrapError("options_arg unexpectedly null.")); + reply(wrapped); + return; + } + const auto& options_arg = std::any_cast( + std::get( + encodable_options_arg)); + const auto& encodable_initial_directory_arg = args.at(1); + const auto* initial_directory_arg = + std::get_if(&encodable_initial_directory_arg); + const auto& encodable_confirm_button_text_arg = args.at(2); + const auto* confirm_button_text_arg = + std::get_if(&encodable_confirm_button_text_arg); + ErrorOr output = api->ShowOpenDialog( + options_arg, initial_directory_arg, confirm_button_text_arg); + if (output.has_error()) { + wrapped.emplace(flutter::EncodableValue("error"), + WrapError(output.error())); + } else { + wrapped.emplace( + flutter::EncodableValue("result"), + flutter::EncodableValue(std::move(output).TakeValue())); + } + } catch (const std::exception& exception) { + wrapped.emplace(flutter::EncodableValue("error"), + WrapError(exception.what())); + } + reply(wrapped); + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = + std::make_unique>( + binary_messenger, + "dev.flutter.pigeon.FileSelectorApi.showSaveDialog", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::EncodableValue& message, + const flutter::MessageReply& reply) { + flutter::EncodableMap wrapped; + try { + const auto& args = std::get(message); + const auto& encodable_options_arg = args.at(0); + if (encodable_options_arg.IsNull()) { + wrapped.emplace(flutter::EncodableValue("error"), + WrapError("options_arg unexpectedly null.")); + reply(wrapped); + return; + } + const auto& options_arg = std::any_cast( + std::get( + encodable_options_arg)); + const auto& encodable_initial_directory_arg = args.at(1); + const auto* initial_directory_arg = + std::get_if(&encodable_initial_directory_arg); + const auto& encodable_suggested_name_arg = args.at(2); + const auto* suggested_name_arg = + std::get_if(&encodable_suggested_name_arg); + const auto& encodable_confirm_button_text_arg = args.at(3); + const auto* confirm_button_text_arg = + std::get_if(&encodable_confirm_button_text_arg); + ErrorOr output = api->ShowSaveDialog( + options_arg, initial_directory_arg, suggested_name_arg, + confirm_button_text_arg); + if (output.has_error()) { + wrapped.emplace(flutter::EncodableValue("error"), + WrapError(output.error())); + } else { + wrapped.emplace( + flutter::EncodableValue("result"), + flutter::EncodableValue(std::move(output).TakeValue())); + } + } catch (const std::exception& exception) { + wrapped.emplace(flutter::EncodableValue("error"), + WrapError(exception.what())); + } + reply(wrapped); + }); + } else { + channel->SetMessageHandler(nullptr); + } + } +} + +flutter::EncodableMap FileSelectorApi::WrapError( + std::string_view error_message) { + return flutter::EncodableMap( + {{flutter::EncodableValue("message"), + flutter::EncodableValue(std::string(error_message))}, + {flutter::EncodableValue("code"), flutter::EncodableValue("Error")}, + {flutter::EncodableValue("details"), flutter::EncodableValue()}}); +} +flutter::EncodableMap FileSelectorApi::WrapError(const FlutterError& error) { + return flutter::EncodableMap( + {{flutter::EncodableValue("message"), + flutter::EncodableValue(error.message())}, + {flutter::EncodableValue("code"), flutter::EncodableValue(error.code())}, + {flutter::EncodableValue("details"), error.details()}}); +} + +} // namespace file_selector_windows diff --git a/packages/file_selector/file_selector_windows/windows/messages.g.h b/packages/file_selector/file_selector_windows/windows/messages.g.h new file mode 100644 index 000000000000..fb496d2d66e2 --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/messages.g.h @@ -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. +// Autogenerated from Pigeon (v3.2.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#ifndef PIGEON_MESSAGES_G_FILE_SELECTOR_WINDOWS_H_ +#define PIGEON_MESSAGES_G_FILE_SELECTOR_WINDOWS_H_ +#include +#include +#include +#include + +#include +#include +#include + +namespace file_selector_windows { + +/* Generated class from Pigeon. */ + +class FlutterError { + public: + FlutterError(const std::string& code) : code_(code) {} + FlutterError(const std::string& code, const std::string& message) + : code_(code), message_(message) {} + 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 FileSelectorApi; + ErrorOr() = default; + T TakeValue() && { return std::get(std::move(v_)); } + + std::variant v_; +}; + +/* Generated class from Pigeon that represents data sent in messages. */ +class TypeGroup { + public: + TypeGroup(); + const std::string& label() const; + void set_label(std::string_view value_arg); + + const flutter::EncodableList& extensions() const; + void set_extensions(const flutter::EncodableList& value_arg); + + private: + TypeGroup(flutter::EncodableMap map); + flutter::EncodableMap ToEncodableMap() const; + friend class FileSelectorApi; + friend class FileSelectorApiCodecSerializer; + std::string label_; + flutter::EncodableList extensions_; +}; + +/* Generated class from Pigeon that represents data sent in messages. */ +class SelectionOptions { + public: + SelectionOptions(); + bool allow_multiple() const; + void set_allow_multiple(bool value_arg); + + bool select_folders() const; + void set_select_folders(bool value_arg); + + const flutter::EncodableList& allowed_types() const; + void set_allowed_types(const flutter::EncodableList& value_arg); + + private: + SelectionOptions(flutter::EncodableMap map); + flutter::EncodableMap ToEncodableMap() const; + friend class FileSelectorApi; + friend class FileSelectorApiCodecSerializer; + bool allow_multiple_; + bool select_folders_; + flutter::EncodableList allowed_types_; +}; + +class FileSelectorApiCodecSerializer : public flutter::StandardCodecSerializer { + public: + inline static FileSelectorApiCodecSerializer& GetInstance() { + static FileSelectorApiCodecSerializer sInstance; + return sInstance; + } + + FileSelectorApiCodecSerializer(); + + public: + void WriteValue(const flutter::EncodableValue& value, + flutter::ByteStreamWriter* stream) const override; + + protected: + flutter::EncodableValue ReadValueOfType( + uint8_t type, flutter::ByteStreamReader* stream) const override; +}; + +/* Generated class from Pigeon that represents a handler of messages from + * Flutter. */ +class FileSelectorApi { + public: + FileSelectorApi(const FileSelectorApi&) = delete; + FileSelectorApi& operator=(const FileSelectorApi&) = delete; + virtual ~FileSelectorApi(){}; + virtual ErrorOr ShowOpenDialog( + const SelectionOptions& options, const std::string* initial_directory, + const std::string* confirm_button_text) = 0; + virtual ErrorOr ShowSaveDialog( + const SelectionOptions& options, const std::string* initial_directory, + const std::string* suggested_name, + const std::string* confirm_button_text) = 0; + + /** The codec used by FileSelectorApi. */ + static const flutter::StandardMessageCodec& GetCodec(); + /** Sets up an instance of `FileSelectorApi` to handle messages through the + * `binary_messenger`. */ + static void SetUp(flutter::BinaryMessenger* binary_messenger, + FileSelectorApi* api); + static flutter::EncodableMap WrapError(std::string_view error_message); + static flutter::EncodableMap WrapError(const FlutterError& error); + + protected: + FileSelectorApi() = default; +}; +} // namespace file_selector_windows +#endif // PIGEON_MESSAGES_G_FILE_SELECTOR_WINDOWS_H_ diff --git a/packages/file_selector/file_selector_windows/windows/string_utils.cpp b/packages/file_selector/file_selector_windows/windows/string_utils.cpp index 933500f34445..6fa7c18403a7 100644 --- a/packages/file_selector/file_selector_windows/windows/string_utils.cpp +++ b/packages/file_selector/file_selector_windows/windows/string_utils.cpp @@ -12,7 +12,7 @@ namespace file_selector_windows { // Converts the given UTF-16 string to UTF-8. -std::string Utf8FromUtf16(const std::wstring& utf16_string) { +std::string Utf8FromUtf16(std::wstring_view utf16_string) { if (utf16_string.empty()) { return std::string(); } @@ -35,7 +35,7 @@ std::string Utf8FromUtf16(const std::wstring& utf16_string) { } // Converts the given UTF-8 string to UTF-16. -std::wstring Utf16FromUtf8(const std::string& utf8_string) { +std::wstring Utf16FromUtf8(std::string_view utf8_string) { if (utf8_string.empty()) { return std::wstring(); } diff --git a/packages/file_selector/file_selector_windows/windows/string_utils.h b/packages/file_selector/file_selector_windows/windows/string_utils.h index 74c7d4f93934..2323a5a589d8 100644 --- a/packages/file_selector/file_selector_windows/windows/string_utils.h +++ b/packages/file_selector/file_selector_windows/windows/string_utils.h @@ -11,10 +11,10 @@ namespace file_selector_windows { // Converts the given UTF-16 string to UTF-8. -std::string Utf8FromUtf16(const std::wstring& utf16_string); +std::string Utf8FromUtf16(std::wstring_view utf16_string); // Converts the given UTF-8 string to UTF-16. -std::wstring Utf16FromUtf8(const std::string& utf8_string); +std::wstring Utf16FromUtf8(std::string_view utf8_string); } // namespace file_selector_windows diff --git a/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp b/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp index 20e1bba2794f..2325a271b777 100644 --- a/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp +++ b/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp @@ -26,25 +26,38 @@ namespace test { namespace { +using flutter::CustomEncodableValue; using flutter::EncodableList; using flutter::EncodableMap; using flutter::EncodableValue; -using flutter::MethodCall; -using ::testing::DoAll; -using ::testing::Pointee; -using ::testing::Return; -using ::testing::SetArgPointee; - -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)); + +// These structs and classes are a workaround for +// https://github.com/flutter/flutter/issues/104286 and +// https://github.com/flutter/flutter/issues/104653. +struct AllowMultipleArg { + bool value = false; + AllowMultipleArg(bool val) : value(val) {} +}; +struct SelectFoldersArg { + bool value = false; + SelectFoldersArg(bool val) : value(val) {} }; +SelectionOptions CreateOptions(AllowMultipleArg allow_multiple, + SelectFoldersArg select_folders, + const EncodableList& allowed_types) { + SelectionOptions options; + options.set_allow_multiple(allow_multiple.value); + options.set_select_folders(select_folders.value); + options.set_allowed_types(allowed_types); + return options; +} +TypeGroup CreateTypeGroup(std::string_view label, + const EncodableList& extensions) { + TypeGroup group; + group.set_label(label); + group.set_extensions(extensions); + return group; +} } // namespace @@ -55,9 +68,6 @@ TEST(FileSelectorPlugin, TestOpenSimple) { ::SHCreateShellItemArrayFromShellItem(fake_selected_file.file(), IID_PPV_ARGS(&fake_result_array)); - std::unique_ptr result = - std::make_unique(); - bool shown = false; MockShow show_validator = [&shown, fake_result_array, fake_window]( const TestFileDialogController& dialog, @@ -73,20 +83,21 @@ TEST(FileSelectorPlugin, TestOpenSimple) { return MockShowResult(fake_result_array); }; - EncodableValue expected_paths(EncodableList({ - EncodableValue(Utf8FromUtf16(fake_selected_file.path())), - })); - // Expect the mock path. - EXPECT_CALL(*result, SuccessInternal(Pointee(expected_paths))); FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - plugin.HandleMethodCall( - MethodCall("openFile", std::make_unique(EncodableMap())), - std::move(result)); + ErrorOr result = plugin.ShowOpenDialog( + CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), + EncodableList()), + nullptr, nullptr); EXPECT_TRUE(shown); + ASSERT_FALSE(result.has_error()); + const EncodableList& paths = result.value(); + EXPECT_EQ(paths.size(), 1); + EXPECT_EQ(std::get(paths[0]), + Utf8FromUtf16(fake_selected_file.path())); } TEST(FileSelectorPlugin, TestOpenWithArguments) { @@ -96,9 +107,6 @@ TEST(FileSelectorPlugin, TestOpenWithArguments) { ::SHCreateShellItemArrayFromShellItem(fake_selected_file.file(), IID_PPV_ARGS(&fake_result_array)); - std::unique_ptr result = - std::make_unique(); - bool shown = false; MockShow show_validator = [&shown, fake_result_array, fake_window]( const TestFileDialogController& dialog, @@ -107,34 +115,31 @@ TEST(FileSelectorPlugin, TestOpenWithArguments) { EXPECT_EQ(parent, fake_window); // Validate arguments. - EXPECT_EQ(dialog.GetDefaultFolderPath(), L"C:\\Program Files"); - EXPECT_EQ(dialog.GetFileName(), L"a name"); + EXPECT_EQ(dialog.GetDialogFolderPath(), L"C:\\Program Files"); + // Make sure that the folder was called via SetFolder, not SetDefaultFolder. + EXPECT_EQ(dialog.GetSetFolderPath(), L"C:\\Program Files"); EXPECT_EQ(dialog.GetOkButtonLabel(), L"Open it!"); return MockShowResult(fake_result_array); }; - EncodableValue expected_paths(EncodableList({ - EncodableValue(Utf8FromUtf16(fake_selected_file.path())), - })); - // Expect the mock path. - EXPECT_CALL(*result, SuccessInternal(Pointee(expected_paths))); FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - plugin.HandleMethodCall( - MethodCall( - "openFile", - std::make_unique(EncodableMap({ - // This directory must exist. - {EncodableValue("initialDirectory"), - EncodableValue("C:\\Program Files")}, - {EncodableValue("suggestedName"), EncodableValue("a name")}, - {EncodableValue("confirmButtonText"), EncodableValue("Open it!")}, - }))), - std::move(result)); + // This directory must exist. + std::string initial_directory("C:\\Program Files"); + std::string confirm_button("Open it!"); + ErrorOr result = plugin.ShowOpenDialog( + CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), + EncodableList()), + &initial_directory, &confirm_button); EXPECT_TRUE(shown); + ASSERT_FALSE(result.has_error()); + const EncodableList& paths = result.value(); + EXPECT_EQ(paths.size(), 1); + EXPECT_EQ(std::get(paths[0]), + Utf8FromUtf16(fake_selected_file.path())); } TEST(FileSelectorPlugin, TestOpenMultiple) { @@ -149,9 +154,6 @@ TEST(FileSelectorPlugin, TestOpenMultiple) { ::SHCreateShellItemArrayFromIDLists(2, fake_selected_files, &fake_result_array); - std::unique_ptr result = - std::make_unique(); - bool shown = false; MockShow show_validator = [&shown, fake_result_array, fake_window]( const TestFileDialogController& dialog, @@ -167,24 +169,23 @@ TEST(FileSelectorPlugin, TestOpenMultiple) { return MockShowResult(fake_result_array); }; - EncodableValue expected_paths(EncodableList({ - EncodableValue(Utf8FromUtf16(fake_selected_file_1.path())), - EncodableValue(Utf8FromUtf16(fake_selected_file_2.path())), - })); - // Expect the mock path. - EXPECT_CALL(*result, SuccessInternal(Pointee(expected_paths))); FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - plugin.HandleMethodCall( - MethodCall("openFile", - std::make_unique(EncodableMap({ - {EncodableValue("multiple"), EncodableValue(true)}, - }))), - std::move(result)); + ErrorOr result = plugin.ShowOpenDialog( + CreateOptions(AllowMultipleArg(true), SelectFoldersArg(false), + EncodableList()), + nullptr, nullptr); EXPECT_TRUE(shown); + ASSERT_FALSE(result.has_error()); + const EncodableList& paths = result.value(); + EXPECT_EQ(paths.size(), 2); + EXPECT_EQ(std::get(paths[0]), + Utf8FromUtf16(fake_selected_file_1.path())); + EXPECT_EQ(std::get(paths[1]), + Utf8FromUtf16(fake_selected_file_2.path())); } TEST(FileSelectorPlugin, TestOpenWithFilter) { @@ -194,27 +195,19 @@ TEST(FileSelectorPlugin, TestOpenWithFilter) { ::SHCreateShellItemArrayFromShellItem(fake_selected_file.file(), IID_PPV_ARGS(&fake_result_array)); - std::unique_ptr result = - std::make_unique(); - - const EncodableValue text_group = EncodableValue(EncodableMap({ - {EncodableValue("label"), EncodableValue("Text")}, - {EncodableValue("extensions"), EncodableValue(EncodableList({ - EncodableValue("txt"), - EncodableValue("json"), - }))}, - })); - const EncodableValue image_group = EncodableValue(EncodableMap({ - {EncodableValue("label"), EncodableValue("Images")}, - {EncodableValue("extensions"), EncodableValue(EncodableList({ - EncodableValue("png"), - EncodableValue("gif"), - EncodableValue("jpeg"), - }))}, - })); - const EncodableValue any_group = EncodableValue(EncodableMap({ - {EncodableValue("label"), EncodableValue("Any")}, - })); + const EncodableValue text_group = + CustomEncodableValue(CreateTypeGroup("Text", EncodableList({ + EncodableValue("txt"), + EncodableValue("json"), + }))); + const EncodableValue image_group = + CustomEncodableValue(CreateTypeGroup("Images", EncodableList({ + EncodableValue("png"), + EncodableValue("gif"), + EncodableValue("jpeg"), + }))); + const EncodableValue any_group = + CustomEncodableValue(CreateTypeGroup("Any", EncodableList())); bool shown = false; MockShow show_validator = [&shown, fake_result_array, fake_window]( @@ -237,35 +230,30 @@ TEST(FileSelectorPlugin, TestOpenWithFilter) { return MockShowResult(fake_result_array); }; - EncodableValue expected_paths(EncodableList({ - EncodableValue(Utf8FromUtf16(fake_selected_file.path())), - })); - // Expect the mock path. - EXPECT_CALL(*result, SuccessInternal(Pointee(expected_paths))); FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - plugin.HandleMethodCall( - MethodCall("openFile", std::make_unique(EncodableMap({ - {EncodableValue("acceptedTypeGroups"), - EncodableValue(EncodableList({ - text_group, - image_group, - any_group, - }))}, - }))), - std::move(result)); + ErrorOr result = plugin.ShowOpenDialog( + CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), + EncodableList({ + text_group, + image_group, + any_group, + })), + nullptr, nullptr); EXPECT_TRUE(shown); + ASSERT_FALSE(result.has_error()); + const EncodableList& paths = result.value(); + EXPECT_EQ(paths.size(), 1); + EXPECT_EQ(std::get(paths[0]), + Utf8FromUtf16(fake_selected_file.path())); } TEST(FileSelectorPlugin, TestOpenCancel) { const HWND fake_window = reinterpret_cast(1337); - std::unique_ptr result = - std::make_unique(); - bool shown = false; MockShow show_validator = [&shown, fake_window]( const TestFileDialogController& dialog, @@ -273,28 +261,25 @@ TEST(FileSelectorPlugin, TestOpenCancel) { shown = true; return MockShowResult(); }; - // Cancel should return a null for the paths. - EncodableValue expected_paths; - // Expect the mock path. - EXPECT_CALL(*result, SuccessInternal(Pointee(expected_paths))); FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - plugin.HandleMethodCall( - MethodCall("openFile", std::make_unique(EncodableMap())), - std::move(result)); + ErrorOr result = plugin.ShowOpenDialog( + CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), + EncodableList()), + nullptr, nullptr); EXPECT_TRUE(shown); + ASSERT_FALSE(result.has_error()); + const EncodableList& paths = result.value(); + EXPECT_EQ(paths.size(), 0); } TEST(FileSelectorPlugin, TestSaveSimple) { const HWND fake_window = reinterpret_cast(1337); ScopedTestShellItem fake_selected_file; - std::unique_ptr result = - std::make_unique(); - bool shown = false; MockShow show_validator = [&shown, fake_result = fake_selected_file.file(), fake_window]( @@ -310,28 +295,27 @@ TEST(FileSelectorPlugin, TestSaveSimple) { return MockShowResult(fake_result); }; - EncodableValue expected_path(Utf8FromUtf16(fake_selected_file.path())); - // Expect the mock path. - EXPECT_CALL(*result, SuccessInternal(Pointee(expected_path))); FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - plugin.HandleMethodCall( - MethodCall("getSavePath", - std::make_unique(EncodableMap())), - std::move(result)); + ErrorOr result = plugin.ShowSaveDialog( + CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), + EncodableList()), + nullptr, nullptr, nullptr); EXPECT_TRUE(shown); + ASSERT_FALSE(result.has_error()); + const EncodableList& paths = result.value(); + EXPECT_EQ(paths.size(), 1); + EXPECT_EQ(std::get(paths[0]), + Utf8FromUtf16(fake_selected_file.path())); } TEST(FileSelectorPlugin, TestSaveWithArguments) { const HWND fake_window = reinterpret_cast(1337); ScopedTestShellItem fake_selected_file; - std::unique_ptr result = - std::make_unique(); - bool shown = false; MockShow show_validator = [&shown, fake_result = fake_selected_file.file(), fake_window]( @@ -340,38 +324,39 @@ TEST(FileSelectorPlugin, TestSaveWithArguments) { EXPECT_EQ(parent, fake_window); // Validate arguments. - EXPECT_EQ(dialog.GetDefaultFolderPath(), L"C:\\Program Files"); + EXPECT_EQ(dialog.GetDialogFolderPath(), L"C:\\Program Files"); + // Make sure that the folder was called via SetFolder, not + // SetDefaultFolder. + EXPECT_EQ(dialog.GetSetFolderPath(), L"C:\\Program Files"); + EXPECT_EQ(dialog.GetFileName(), L"a name"); EXPECT_EQ(dialog.GetOkButtonLabel(), L"Save it!"); return MockShowResult(fake_result); }; - EncodableValue expected_path(Utf8FromUtf16(fake_selected_file.path())); - // Expect the mock path. - EXPECT_CALL(*result, SuccessInternal(Pointee(expected_path))); FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - plugin.HandleMethodCall( - MethodCall( - "getSavePath", - std::make_unique(EncodableMap({ - // This directory must exist. - {EncodableValue("initialDirectory"), - EncodableValue("C:\\Program Files")}, - {EncodableValue("confirmButtonText"), EncodableValue("Save it!")}, - }))), - std::move(result)); + // This directory must exist. + std::string initial_directory("C:\\Program Files"); + std::string suggested_name("a name"); + std::string confirm_button("Save it!"); + ErrorOr result = plugin.ShowSaveDialog( + CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), + EncodableList()), + &initial_directory, &suggested_name, &confirm_button); EXPECT_TRUE(shown); + ASSERT_FALSE(result.has_error()); + const EncodableList& paths = result.value(); + EXPECT_EQ(paths.size(), 1); + EXPECT_EQ(std::get(paths[0]), + Utf8FromUtf16(fake_selected_file.path())); } TEST(FileSelectorPlugin, TestSaveCancel) { const HWND fake_window = reinterpret_cast(1337); - std::unique_ptr result = - std::make_unique(); - bool shown = false; MockShow show_validator = [&shown, fake_window]( const TestFileDialogController& dialog, @@ -379,20 +364,19 @@ TEST(FileSelectorPlugin, TestSaveCancel) { shown = true; return MockShowResult(); }; - // Cancel should return a null for the path. - EncodableValue expected_path; - // Expect the mock path. - EXPECT_CALL(*result, SuccessInternal(Pointee(expected_path))); FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - plugin.HandleMethodCall( - MethodCall("getSavePath", - std::make_unique(EncodableMap())), - std::move(result)); + ErrorOr result = plugin.ShowSaveDialog( + CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false), + EncodableList()), + nullptr, nullptr, nullptr); EXPECT_TRUE(shown); + ASSERT_FALSE(result.has_error()); + const EncodableList& paths = result.value(); + EXPECT_EQ(paths.size(), 0); } TEST(FileSelectorPlugin, TestGetDirectorySimple) { @@ -405,9 +389,6 @@ TEST(FileSelectorPlugin, TestGetDirectorySimple) { ::SHCreateShellItemArrayFromShellItem(fake_selected_directory, IID_PPV_ARGS(&fake_result_array)); - std::unique_ptr result = - std::make_unique(); - bool shown = false; MockShow show_validator = [&shown, fake_result_array, fake_window]( const TestFileDialogController& dialog, @@ -423,27 +404,25 @@ TEST(FileSelectorPlugin, TestGetDirectorySimple) { return MockShowResult(fake_result_array); }; - EncodableValue expected_path("C:\\Program Files"); - // Expect the mock path. - EXPECT_CALL(*result, SuccessInternal(Pointee(expected_path))); FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - plugin.HandleMethodCall( - MethodCall("getDirectoryPath", - std::make_unique(EncodableMap())), - std::move(result)); + ErrorOr result = plugin.ShowOpenDialog( + CreateOptions(AllowMultipleArg(false), SelectFoldersArg(true), + EncodableList()), + nullptr, nullptr); EXPECT_TRUE(shown); + ASSERT_FALSE(result.has_error()); + const EncodableList& paths = result.value(); + EXPECT_EQ(paths.size(), 1); + EXPECT_EQ(std::get(paths[0]), "C:\\Program Files"); } TEST(FileSelectorPlugin, TestGetDirectoryCancel) { const HWND fake_window = reinterpret_cast(1337); - std::unique_ptr result = - std::make_unique(); - bool shown = false; MockShow show_validator = [&shown, fake_window]( const TestFileDialogController& dialog, @@ -451,20 +430,19 @@ TEST(FileSelectorPlugin, TestGetDirectoryCancel) { shown = true; return MockShowResult(); }; - // Cancel should return a null for the path. - EncodableValue expected_path; - // Expect the mock path. - EXPECT_CALL(*result, SuccessInternal(Pointee(expected_path))); FileSelectorPlugin plugin( [fake_window] { return fake_window; }, std::make_unique(show_validator)); - plugin.HandleMethodCall( - MethodCall("getDirectoryPath", - std::make_unique(EncodableMap())), - std::move(result)); + ErrorOr result = plugin.ShowOpenDialog( + CreateOptions(AllowMultipleArg(false), SelectFoldersArg(true), + EncodableList()), + nullptr, nullptr); EXPECT_TRUE(shown); + ASSERT_FALSE(result.has_error()); + const EncodableList& paths = result.value(); + EXPECT_EQ(paths.size(), 0); } } // namespace test diff --git a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp index a98b686ddd6e..15065f916c8b 100644 --- a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp +++ b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp @@ -20,6 +20,17 @@ TestFileDialogController::TestFileDialogController(IFileDialog* dialog, TestFileDialogController::~TestFileDialogController() {} +HRESULT TestFileDialogController::SetFolder(IShellItem* folder) { + wchar_t* path_chars = nullptr; + if (SUCCEEDED(folder->GetDisplayName(SIGDN_FILESYSPATH, &path_chars))) { + set_folder_path_ = path_chars; + } else { + set_folder_path_ = L""; + } + + return FileDialogController::SetFolder(folder); +} + HRESULT TestFileDialogController::SetFileTypes(UINT count, COMDLG_FILTERSPEC* filters) { filter_groups_.clear(); @@ -56,7 +67,11 @@ HRESULT TestFileDialogController::GetResults( return S_OK; } -std::wstring TestFileDialogController::GetDefaultFolderPath() const { +std::wstring TestFileDialogController::GetSetFolderPath() const { + return set_folder_path_; +} + +std::wstring TestFileDialogController::GetDialogFolderPath() const { IShellItemPtr item; if (!SUCCEEDED(dialog_->GetFolder(&item))) { return L""; diff --git a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h index 2e7292bad4b2..1c221fc219f9 100644 --- a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h +++ b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h @@ -51,6 +51,7 @@ class TestFileDialogController : public FileDialogController { ~TestFileDialogController(); // FileDialogController: + HRESULT SetFolder(IShellItem* folder) override; HRESULT SetFileTypes(UINT count, COMDLG_FILTERSPEC* filters) override; HRESULT SetOkButtonLabel(const wchar_t* text) override; HRESULT Show(HWND parent) override; @@ -58,7 +59,14 @@ class TestFileDialogController : public FileDialogController { HRESULT GetResults(IShellItemArray** out_items) const override; // Accessors for validating IFileDialogController setter calls. - std::wstring GetDefaultFolderPath() const; + // Gets the folder path set by FileDialogController::SetFolder. + // + // This exists because there are multiple ways that the value returned by + // GetDialogFolderPath can be changed, so this allows specifically validating + // calls to SetFolder. + std::wstring GetSetFolderPath() const; + // Gets dialog folder path by calling IFileDialog::GetFolder. + std::wstring GetDialogFolderPath() const; std::wstring GetFileName() const; const std::vector& GetFileTypes() const; std::wstring GetOkButtonLabel() const; @@ -70,6 +78,7 @@ class TestFileDialogController : public FileDialogController { // The last set values, for IFileDialog properties that have setters but no // corresponding getters. + std::wstring set_folder_path_; std::wstring ok_button_label_; std::vector filter_groups_; }; diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index 8b110b74acfc..81202f8159de 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,3 +1,17 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 2.0.7 + +* Bumps gradle from 3.5.0 to 7.2.1. + +## 2.0.6 + +* Adds OS version support information to README. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.0.5 * Updates compileSdkVersion to 31. diff --git a/packages/flutter_plugin_android_lifecycle/README.md b/packages/flutter_plugin_android_lifecycle/README.md index 3290140f4e5e..2475230d413b 100644 --- a/packages/flutter_plugin_android_lifecycle/README.md +++ b/packages/flutter_plugin_android_lifecycle/README.md @@ -9,6 +9,10 @@ The purpose of having this plugin instead of exposing an Android `Lifecycle` obj Android embedding plugins API is to force plugins to have a pub constraint that signifies the major version of the Android `Lifecycle` API they expect. +| | Android | +|-------------|---------| +| **Support** | SDK 16+ | + ## Installation Add `flutter_plugin_android_lifecycle` as a [dependency in your pubspec.yaml file](https://flutter.dev/using-packages/). @@ -32,7 +36,7 @@ public class MyPlugin implements FlutterPlugin, ActivityAware { Lifecycle lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); // Use lifecycle as desired. } - + //... } ``` diff --git a/packages/flutter_plugin_android_lifecycle/android/build.gradle b/packages/flutter_plugin_android_lifecycle/android/build.gradle index 7f44c6584456..5786a74e2e78 100644 --- a/packages/flutter_plugin_android_lifecycle/android/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.2.1' } } @@ -30,6 +30,8 @@ android { consumerProguardFiles 'proguard.txt' } lintOptions { + // TODO(stuartmorgan): Enable when gradle is updated. + // disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' } @@ -53,7 +55,7 @@ android { } dependencies { - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:1.10.19' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:4.7.0' } diff --git a/packages/flutter_plugin_android_lifecycle/example/android/app/build.gradle b/packages/flutter_plugin_android_lifecycle/example/android/app/build.gradle index bc87d03f1aeb..e96ede6844ff 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/build.gradle @@ -55,7 +55,7 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/packages/flutter_plugin_android_lifecycle/example/android/build.gradle b/packages/flutter_plugin_android_lifecycle/example/android/build.gradle index 456d020f6e2c..c21bff8e0a2f 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.0.1' } } diff --git a/packages/flutter_plugin_android_lifecycle/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_plugin_android_lifecycle/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146b7318..b8793d3c0d69 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/flutter_plugin_android_lifecycle/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-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart b/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart index 1d329a02f93b..1198c6f01806 100644 --- a/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart +++ b/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart @@ -10,6 +10,6 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('loads', (WidgetTester tester) async { - await tester.pumpWidget(MyApp()); + await tester.pumpWidget(const MyApp()); }); } diff --git a/packages/flutter_plugin_android_lifecycle/example/lib/main.dart b/packages/flutter_plugin_android_lifecycle/example/lib/main.dart index 3ef6794dfad2..c465b3b687f2 100644 --- a/packages/flutter_plugin_android_lifecycle/example/lib/main.dart +++ b/packages/flutter_plugin_android_lifecycle/example/lib/main.dart @@ -2,13 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: public_member_api_docs - import 'package:flutter/material.dart'; -void main() => runApp(MyApp()); +void main() => runApp(const MyApp()); +/// MyApp is the Main Application. class MyApp extends StatelessWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( diff --git a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml index 0c88cd2c5531..e732497eee95 100644 --- a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml @@ -21,7 +21,6 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.8.0 flutter: uses-material-design: true diff --git a/packages/flutter_plugin_android_lifecycle/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/pubspec.yaml index b359cc55c351..3a6a2e017b53 100644 --- a/packages/flutter_plugin_android_lifecycle/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/pubspec.yaml @@ -2,11 +2,11 @@ name: flutter_plugin_android_lifecycle description: Flutter plugin for accessing an Android Lifecycle within other plugins. repository: https://github.com/flutter/plugins/tree/main/packages/flutter_plugin_android_lifecycle issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_plugin_android_lifecycle%22 -version: 2.0.5 +version: 2.0.7 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: @@ -22,4 +22,3 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 565bf8412c61..3707aa86e95a 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,67 @@ +## NEXT + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. + +## 2.2.1 + +* Updates imports for `prefer_relative_imports`. + +## 2.2.0 + +* Deprecates `AndroidGoogleMapsFlutter.useAndroidViewSurface` in favor of + [setting the flag directly in the Android implementation](https://pub.dev/packages/google_maps_flutter_android#display-mode). +* Updates minimum Flutter version to 2.10. + +## 2.1.12 + +* Fixes violations of new analysis option use_named_constants. + +## 2.1.11 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. +* Moves Android and iOS implementations to federated packages. + +## 2.1.10 + +* Avoids map shift when scrolling on iOS. + +## 2.1.9 + +* Updates integration tests to use the new inspector interface. +* Removes obsolete test-only method for accessing a map controller's method channel. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). + +## 2.1.8 + +* Switches to new platform interface versions of `buildView` and + `updateOptions`. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/104231). + +## 2.1.7 + +* Objective-C code cleanup. + +## 2.1.6 + +* Fixes issue in Flutter v3.0.0 where some updates to the map don't take effect on Android. +* Fixes iOS native unit tests on M1 devices. +* Minor fixes for new analysis options. + +## 2.1.5 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 2.1.4 + +* Updates Android Google maps sdk version to `18.0.2`. +* Adds OS version support information to README. + +## 2.1.3 + +* Fixes iOS crash on `EXC_BAD_ACCESS KERN_PROTECTION_FAILURE` if the map frame changes long after creation. + ## 2.1.2 * Removes dependencies from `pubspec.yaml` that are only needed in `example/pubspec.yaml` @@ -210,7 +274,7 @@ GoogleMapController is now uniformly driven by implementing `DefaultLifecycleObs ## 0.5.26+1 -* Removes a errorneously added method from the GoogleMapController.h header file. +* Removes an erroneously added method from the GoogleMapController.h header file. ## 0.5.26 diff --git a/packages/google_maps_flutter/google_maps_flutter/README.md b/packages/google_maps_flutter/google_maps_flutter/README.md index 038126f4fdd0..58726a1faaa1 100644 --- a/packages/google_maps_flutter/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/google_maps_flutter/README.md @@ -4,6 +4,10 @@ A Flutter plugin that provides a [Google Maps](https://developers.google.com/maps/) widget. +| | Android | iOS | +|-------------|---------|--------| +| **Support** | SDK 20+ | iOS 9+ | + ## Usage To use this plugin, add `google_maps_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). @@ -46,21 +50,15 @@ This means that app will only be available for users that run Android SDK 20 or android:value="YOUR KEY HERE"/> ``` -#### Hybrid Composition - -To use [Hybrid Composition](https://flutter.dev/docs/development/platform-integration/platform-views) -to render the `GoogleMap` widget on Android, set `AndroidGoogleMapsFlutter.useAndroidViewSurface` to -true. +#### Display Mode -```dart -if (defaultTargetPlatform == TargetPlatform.android) { - AndroidGoogleMapsFlutter.useAndroidViewSurface = true; -} -``` +The Android implementation supports multiple +[platform view display modes](https://flutter.dev/docs/development/platform-integration/platform-views). +For details, see [the Android README](https://pub.dev/packages/google_maps_flutter_android#display-mode). ### iOS -This plugin requires iOS 9.0 or higher. To set up, specify your API key in the application delegate `ios/Runner/AppDelegate.m`: +To set up, specify your API key in the application delegate `ios/Runner/AppDelegate.m`: ```objectivec #include "AppDelegate.h" diff --git a/packages/google_maps_flutter/google_maps_flutter/android/settings.gradle b/packages/google_maps_flutter/google_maps_flutter/android/settings.gradle deleted file mode 100644 index dbceadf4c145..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'google_maps_flutter' diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java deleted file mode 100644 index 6bda085caf46..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java +++ /dev/null @@ -1,61 +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. - -package io.flutter.plugins.googlemaps; - -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import android.content.Context; -import android.os.Build; -import androidx.activity.ComponentActivity; -import androidx.test.core.app.ApplicationProvider; -import com.google.android.gms.maps.GoogleMap; -import io.flutter.plugin.common.BinaryMessenger; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -@RunWith(RobolectricTestRunner.class) -@Config(sdk = Build.VERSION_CODES.P) -public class GoogleMapControllerTest { - - private Context context; - private ComponentActivity activity; - private GoogleMapController googleMapController; - - @Mock BinaryMessenger mockMessenger; - @Mock GoogleMap mockGoogleMap; - - @Before - public void before() { - MockitoAnnotations.initMocks(this); - context = ApplicationProvider.getApplicationContext(); - activity = Robolectric.setupActivity(ComponentActivity.class); - googleMapController = - new GoogleMapController(0, context, mockMessenger, activity::getLifecycle, null); - googleMapController.init(); - } - - @Test - public void DisposeReleaseTheMap() throws InterruptedException { - googleMapController.onMapReady(mockGoogleMap); - assertTrue(googleMapController != null); - googleMapController.dispose(); - assertNull(googleMapController.getView()); - } - - @Test - public void OnDestroyReleaseTheMap() throws InterruptedException { - googleMapController.onMapReady(mockGoogleMap); - assertTrue(googleMapController != null); - googleMapController.onDestroy(activity); - assertNull(googleMapController.getView()); - } -} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/README.md b/packages/google_maps_flutter/google_maps_flutter/example/README.md index b92b9c326143..c8852649b065 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/README.md +++ b/packages/google_maps_flutter/google_maps_flutter/example/README.md @@ -1,8 +1,3 @@ # google_maps_flutter_example Demonstrates how to use the google_maps_flutter plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/build.gradle b/packages/google_maps_flutter/google_maps_flutter/example/android/app/build.gradle index e46f18b41c22..f6d29f63fadc 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/build.gradle @@ -60,7 +60,7 @@ android { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' api 'androidx.test:core:1.2.0' diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/google_maps_flutter/google_maps_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter/example/android/build.gradle index 456d020f6e2c..c21bff8e0a2f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.0.1' } } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_maps_flutter/google_maps_flutter/example/android/gradle/wrapper/gradle-wrapper.properties index 9ec7236c631e..b8793d3c0d69 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_maps_flutter/google_maps_flutter/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-5.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart deleted file mode 100644 index 34baa902ab1b..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.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 'dart:typed_data'; -import 'package:flutter/services.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; - -/// Inspect Google Maps state using the platform SDK. -/// -/// This class is primarily used for testing. The methods on this -/// class should call "getters" on the GoogleMap object or equivalent -/// on the platform side. -class GoogleMapInspector { - GoogleMapInspector(this._channel); - - final MethodChannel _channel; - - Future isCompassEnabled() async { - return await _channel.invokeMethod('map#isCompassEnabled'); - } - - Future isMapToolbarEnabled() async { - return await _channel.invokeMethod('map#isMapToolbarEnabled'); - } - - Future getMinMaxZoomLevels() async { - final List zoomLevels = - (await _channel.invokeMethod>('map#getMinMaxZoomLevels'))! - .cast(); - return MinMaxZoomPreference(zoomLevels[0], zoomLevels[1]); - } - - Future getZoomLevel() async { - final double? zoomLevel = - await _channel.invokeMethod('map#getZoomLevel'); - return zoomLevel; - } - - Future isZoomGesturesEnabled() async { - return await _channel.invokeMethod('map#isZoomGesturesEnabled'); - } - - Future isZoomControlsEnabled() async { - return await _channel.invokeMethod('map#isZoomControlsEnabled'); - } - - Future isLiteModeEnabled() async { - return await _channel.invokeMethod('map#isLiteModeEnabled'); - } - - Future isRotateGesturesEnabled() async { - return await _channel.invokeMethod('map#isRotateGesturesEnabled'); - } - - Future isTiltGesturesEnabled() async { - return await _channel.invokeMethod('map#isTiltGesturesEnabled'); - } - - Future isScrollGesturesEnabled() async { - return await _channel.invokeMethod('map#isScrollGesturesEnabled'); - } - - Future isMyLocationButtonEnabled() async { - return await _channel.invokeMethod('map#isMyLocationButtonEnabled'); - } - - Future isTrafficEnabled() async { - return await _channel.invokeMethod('map#isTrafficEnabled'); - } - - Future isBuildingsEnabled() async { - return await _channel.invokeMethod('map#isBuildingsEnabled'); - } - - Future takeSnapshot() async { - return await _channel.invokeMethod('map#takeSnapshot'); - } - - Future?> getTileOverlayInfo(String id) async { - return await _channel.invokeMapMethod( - 'map#getTileOverlayInfo', { - 'tileOverlayId': id, - }); - } -} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart index a007dddd9188..38a02ea0d8f1 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart @@ -8,13 +8,11 @@ import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:integration_test/integration_test.dart'; -import 'google_map_inspector.dart'; - const LatLng _kInitialMapCenter = LatLng(0, 0); const double _kInitialZoomLevel = 5; const CameraPosition _kInitialCameraPosition = @@ -22,11 +20,33 @@ const CameraPosition _kInitialCameraPosition = void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + GoogleMapsFlutterPlatform.instance.enableDebugInspection(); + + // Repeatedly checks an asynchronous value against a test condition, waiting + // one frame between each check, returing the value if it passes the predicate + // before [maxTries] is reached. + // + // Returns null if the predicate is never satisfied. + // + // This is useful for cases where the Maps SDK has some internally + // asynchronous operation that we don't have visibility into (e.g., native UI + // animations). + Future waitForValueMatchingPredicate(WidgetTester tester, + Future Function() getValue, bool Function(T) predicate, + {int maxTries = 100}) async { + for (int i = 0; i < maxTries; i++) { + final T value = await getValue(); + if (predicate(value)) { + return value; + } + await tester.pump(); + } + return null; + } testWidgets('testCompassToggle', (WidgetTester tester) async { final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( @@ -34,16 +54,15 @@ void main() { initialCameraPosition: _kInitialCameraPosition, compassEnabled: false, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; - bool? compassEnabled = await inspector.isCompassEnabled(); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool compassEnabled = await inspector.isCompassEnabled(mapId: mapId); expect(compassEnabled, false); await tester.pumpWidget(Directionality( @@ -51,21 +70,19 @@ void main() { child: GoogleMap( key: key, initialCameraPosition: _kInitialCameraPosition, - compassEnabled: true, onMapCreated: (GoogleMapController controller) { fail('OnMapCreated should get called only once.'); }, ), )); - compassEnabled = await inspector.isCompassEnabled(); + compassEnabled = await inspector.isCompassEnabled(mapId: mapId); expect(compassEnabled, true); }); testWidgets('testMapToolbarToggle', (WidgetTester tester) async { final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -74,16 +91,15 @@ void main() { initialCameraPosition: _kInitialCameraPosition, mapToolbarEnabled: false, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; - bool? mapToolbarEnabled = await inspector.isMapToolbarEnabled(); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool mapToolbarEnabled = await inspector.isMapToolbarEnabled(mapId: mapId); expect(mapToolbarEnabled, false); await tester.pumpWidget(Directionality( @@ -91,14 +107,13 @@ void main() { child: GoogleMap( key: key, initialCameraPosition: _kInitialCameraPosition, - mapToolbarEnabled: true, onMapCreated: (GoogleMapController controller) { fail('OnMapCreated should get called only once.'); }, ), )); - mapToolbarEnabled = await inspector.isMapToolbarEnabled(); + mapToolbarEnabled = await inspector.isMapToolbarEnabled(mapId: mapId); expect(mapToolbarEnabled, Platform.isAndroid); }); @@ -114,9 +129,8 @@ void main() { // // Thus we test iOS and Android a little differently here. final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - late GoogleMapController controller; + final Completer controllerCompleter = + Completer(); const MinMaxZoomPreference initialZoomLevel = MinMaxZoomPreference(4, 8); const MinMaxZoomPreference finalZoomLevel = MinMaxZoomPreference(6, 10); @@ -128,30 +142,28 @@ void main() { initialCameraPosition: _kInitialCameraPosition, minMaxZoomPreference: initialZoomLevel, onMapCreated: (GoogleMapController c) async { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(c.channel!); - controller = c; - inspectorCompleter.complete(inspector); + controllerCompleter.complete(c); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; + final GoogleMapController controller = await controllerCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; if (Platform.isIOS) { final MinMaxZoomPreference zoomLevel = - await inspector.getMinMaxZoomLevels(); + await inspector.getMinMaxZoomLevels(mapId: controller.mapId); expect(zoomLevel, equals(initialZoomLevel)); } else if (Platform.isAndroid) { await controller.moveCamera(CameraUpdate.zoomTo(15)); await tester.pumpAndSettle(); - double? zoomLevel = await inspector.getZoomLevel(); + double? zoomLevel = await controller.getZoomLevel(); expect(zoomLevel, equals(initialZoomLevel.maxZoom)); await controller.moveCamera(CameraUpdate.zoomTo(1)); await tester.pumpAndSettle(); - zoomLevel = await inspector.getZoomLevel(); + zoomLevel = await controller.getZoomLevel(); expect(zoomLevel, equals(initialZoomLevel.minZoom)); } @@ -169,25 +181,24 @@ void main() { if (Platform.isIOS) { final MinMaxZoomPreference zoomLevel = - await inspector.getMinMaxZoomLevels(); + await inspector.getMinMaxZoomLevels(mapId: controller.mapId); expect(zoomLevel, equals(finalZoomLevel)); } else { await controller.moveCamera(CameraUpdate.zoomTo(15)); await tester.pumpAndSettle(); - double? zoomLevel = await inspector.getZoomLevel(); + double? zoomLevel = await controller.getZoomLevel(); expect(zoomLevel, equals(finalZoomLevel.maxZoom)); await controller.moveCamera(CameraUpdate.zoomTo(1)); await tester.pumpAndSettle(); - zoomLevel = await inspector.getZoomLevel(); + zoomLevel = await controller.getZoomLevel(); expect(zoomLevel, equals(finalZoomLevel.minZoom)); } }); testWidgets('testZoomGesturesEnabled', (WidgetTester tester) async { final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -196,16 +207,16 @@ void main() { initialCameraPosition: _kInitialCameraPosition, zoomGesturesEnabled: false, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; - bool? zoomGesturesEnabled = await inspector.isZoomGesturesEnabled(); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool zoomGesturesEnabled = + await inspector.areZoomGesturesEnabled(mapId: mapId); expect(zoomGesturesEnabled, false); await tester.pumpWidget(Directionality( @@ -213,21 +224,19 @@ void main() { child: GoogleMap( key: key, initialCameraPosition: _kInitialCameraPosition, - zoomGesturesEnabled: true, onMapCreated: (GoogleMapController controller) { fail('OnMapCreated should get called only once.'); }, ), )); - zoomGesturesEnabled = await inspector.isZoomGesturesEnabled(); + zoomGesturesEnabled = await inspector.areZoomGesturesEnabled(mapId: mapId); expect(zoomGesturesEnabled, true); }); testWidgets('testZoomControlsEnabled', (WidgetTester tester) async { final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -235,16 +244,16 @@ void main() { key: key, initialCameraPosition: _kInitialCameraPosition, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; - bool? zoomControlsEnabled = await inspector.isZoomControlsEnabled(); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool zoomControlsEnabled = + await inspector.areZoomControlsEnabled(mapId: mapId); expect(zoomControlsEnabled, !Platform.isIOS); /// Zoom Controls functionality is not available on iOS at the moment. @@ -261,33 +270,31 @@ void main() { ), )); - zoomControlsEnabled = await inspector.isZoomControlsEnabled(); + zoomControlsEnabled = + await inspector.areZoomControlsEnabled(mapId: mapId); expect(zoomControlsEnabled, false); } }); testWidgets('testLiteModeEnabled', (WidgetTester tester) async { final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, initialCameraPosition: _kInitialCameraPosition, - liteModeEnabled: false, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; - bool? liteModeEnabled = await inspector.isLiteModeEnabled(); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool liteModeEnabled = await inspector.isLiteModeEnabled(mapId: mapId); expect(liteModeEnabled, false); await tester.pumpWidget(Directionality( @@ -302,14 +309,13 @@ void main() { ), )); - liteModeEnabled = await inspector.isLiteModeEnabled(); + liteModeEnabled = await inspector.isLiteModeEnabled(mapId: mapId); expect(liteModeEnabled, true); }, skip: !Platform.isAndroid); testWidgets('testRotateGesturesEnabled', (WidgetTester tester) async { final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -318,16 +324,16 @@ void main() { initialCameraPosition: _kInitialCameraPosition, rotateGesturesEnabled: false, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; - bool? rotateGesturesEnabled = await inspector.isRotateGesturesEnabled(); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool rotateGesturesEnabled = + await inspector.areRotateGesturesEnabled(mapId: mapId); expect(rotateGesturesEnabled, false); await tester.pumpWidget(Directionality( @@ -335,21 +341,20 @@ void main() { child: GoogleMap( key: key, initialCameraPosition: _kInitialCameraPosition, - rotateGesturesEnabled: true, onMapCreated: (GoogleMapController controller) { fail('OnMapCreated should get called only once.'); }, ), )); - rotateGesturesEnabled = await inspector.isRotateGesturesEnabled(); + rotateGesturesEnabled = + await inspector.areRotateGesturesEnabled(mapId: mapId); expect(rotateGesturesEnabled, true); }); testWidgets('testTiltGesturesEnabled', (WidgetTester tester) async { final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -358,16 +363,16 @@ void main() { initialCameraPosition: _kInitialCameraPosition, tiltGesturesEnabled: false, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; - bool? tiltGesturesEnabled = await inspector.isTiltGesturesEnabled(); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool tiltGesturesEnabled = + await inspector.areTiltGesturesEnabled(mapId: mapId); expect(tiltGesturesEnabled, false); await tester.pumpWidget(Directionality( @@ -375,21 +380,19 @@ void main() { child: GoogleMap( key: key, initialCameraPosition: _kInitialCameraPosition, - tiltGesturesEnabled: true, onMapCreated: (GoogleMapController controller) { fail('OnMapCreated should get called only once.'); }, ), )); - tiltGesturesEnabled = await inspector.isTiltGesturesEnabled(); + tiltGesturesEnabled = await inspector.areTiltGesturesEnabled(mapId: mapId); expect(tiltGesturesEnabled, true); }); testWidgets('testScrollGesturesEnabled', (WidgetTester tester) async { final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -398,16 +401,16 @@ void main() { initialCameraPosition: _kInitialCameraPosition, scrollGesturesEnabled: false, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; - bool? scrollGesturesEnabled = await inspector.isScrollGesturesEnabled(); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool scrollGesturesEnabled = + await inspector.areScrollGesturesEnabled(mapId: mapId); expect(scrollGesturesEnabled, false); await tester.pumpWidget(Directionality( @@ -415,19 +418,20 @@ void main() { child: GoogleMap( key: key, initialCameraPosition: _kInitialCameraPosition, - scrollGesturesEnabled: true, onMapCreated: (GoogleMapController controller) { fail('OnMapCreated should get called only once.'); }, ), )); - scrollGesturesEnabled = await inspector.isScrollGesturesEnabled(); + scrollGesturesEnabled = + await inspector.areScrollGesturesEnabled(mapId: mapId); expect(scrollGesturesEnabled, true); }); testWidgets('testInitialCenterLocationAtCenter', (WidgetTester tester) async { - await tester.binding.setSurfaceSize(const Size(800.0, 600.0)); + await tester.binding.setSurfaceSize(const Size(800, 600)); + final Completer mapControllerCompleter = Completer(); final Key key = GlobalKey(); @@ -499,12 +503,13 @@ void main() { final GoogleMapController mapController = await mapControllerCompleter.future; + // Wait for the visible region to be non-zero. final LatLngBounds firstVisibleRegion = - await mapController.getVisibleRegion(); - - expect(firstVisibleRegion, isNotNull); - expect(firstVisibleRegion.southwest, isNotNull); - expect(firstVisibleRegion.northeast, isNotNull); + await waitForValueMatchingPredicate( + tester, + () => mapController.getVisibleRegion(), + (LatLngBounds bounds) => bounds != zeroLatLngBounds) ?? + zeroLatLngBounds; expect(firstVisibleRegion, isNot(zeroLatLngBounds)); expect(firstVisibleRegion.contains(_kInitialMapCenter), isTrue); @@ -535,9 +540,6 @@ void main() { final LatLngBounds secondVisibleRegion = await mapController.getVisibleRegion(); - expect(secondVisibleRegion, isNotNull); - expect(secondVisibleRegion.southwest, isNotNull); - expect(secondVisibleRegion.northeast, isNotNull); expect(secondVisibleRegion, isNot(zeroLatLngBounds)); expect(firstVisibleRegion, isNot(secondVisibleRegion)); @@ -546,8 +548,7 @@ void main() { testWidgets('testTraffic', (WidgetTester tester) async { final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -556,16 +557,15 @@ void main() { initialCameraPosition: _kInitialCameraPosition, trafficEnabled: true, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; - bool? isTrafficEnabled = await inspector.isTrafficEnabled(); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool isTrafficEnabled = await inspector.isTrafficEnabled(mapId: mapId); expect(isTrafficEnabled, true); await tester.pumpWidget(Directionality( @@ -573,66 +573,60 @@ void main() { child: GoogleMap( key: key, initialCameraPosition: _kInitialCameraPosition, - trafficEnabled: false, onMapCreated: (GoogleMapController controller) { fail('OnMapCreated should get called only once.'); }, ), )); - isTrafficEnabled = await inspector.isTrafficEnabled(); + isTrafficEnabled = await inspector.isTrafficEnabled(mapId: mapId); expect(isTrafficEnabled, false); }); testWidgets('testBuildings', (WidgetTester tester) async { final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, initialCameraPosition: _kInitialCameraPosition, - buildingsEnabled: true, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; - final bool? isBuildingsEnabled = await inspector.isBuildingsEnabled(); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + final bool isBuildingsEnabled = + await inspector.areBuildingsEnabled(mapId: mapId); expect(isBuildingsEnabled, true); }); // Location button tests are skipped in Android because we don't have location permission to test. testWidgets('testMyLocationButtonToggle', (WidgetTester tester) async { final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, initialCameraPosition: _kInitialCameraPosition, - myLocationButtonEnabled: true, - myLocationEnabled: false, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; - bool? myLocationButtonEnabled = await inspector.isMyLocationButtonEnabled(); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(mapId: mapId); expect(myLocationButtonEnabled, true); await tester.pumpWidget(Directionality( @@ -641,22 +635,21 @@ void main() { key: key, initialCameraPosition: _kInitialCameraPosition, myLocationButtonEnabled: false, - myLocationEnabled: false, onMapCreated: (GoogleMapController controller) { fail('OnMapCreated should get called only once.'); }, ), )); - myLocationButtonEnabled = await inspector.isMyLocationButtonEnabled(); + myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(mapId: mapId); expect(myLocationButtonEnabled, false); }, skip: Platform.isAndroid); testWidgets('testMyLocationButton initial value false', (WidgetTester tester) async { final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, @@ -664,47 +657,41 @@ void main() { key: key, initialCameraPosition: _kInitialCameraPosition, myLocationButtonEnabled: false, - myLocationEnabled: false, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; - final bool? myLocationButtonEnabled = - await inspector.isMyLocationButtonEnabled(); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + final bool myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(mapId: mapId); expect(myLocationButtonEnabled, false); }, skip: Platform.isAndroid); testWidgets('testMyLocationButton initial value true', (WidgetTester tester) async { final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GoogleMap( key: key, initialCameraPosition: _kInitialCameraPosition, - myLocationButtonEnabled: true, - myLocationEnabled: false, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), )); - final GoogleMapInspector inspector = await inspectorCompleter.future; - final bool? myLocationButtonEnabled = - await inspector.isMyLocationButtonEnabled(); + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + final bool myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(mapId: mapId); expect(myLocationButtonEnabled, true); }, skip: Platform.isAndroid); @@ -939,7 +926,13 @@ void main() { expect(iwVisibleStatus, false); await controller.showMarkerInfoWindow(marker.markerId); - iwVisibleStatus = await controller.isMarkerInfoWindowShown(marker.markerId); + // The Maps SDK doesn't always return true for whether it is shown + // immediately after showing it, so wait for it to report as shown. + iwVisibleStatus = await waitForValueMatchingPredicate( + tester, + () => controller.isMarkerInfoWindowShown(marker.markerId), + (bool visible) => visible) ?? + false; expect(iwVisibleStatus, true); await controller.hideMarkerInfoWindow(marker.markerId); @@ -961,8 +954,8 @@ void main() { }); testWidgets('testTakeSnapshot', (WidgetTester tester) async { - final Completer inspectorCompleter = - Completer(); + final Completer controllerCompleter = + Completer(); await tester.pumpWidget( Directionality( @@ -970,10 +963,7 @@ void main() { child: GoogleMap( initialCameraPosition: _kInitialCameraPosition, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + controllerCompleter.complete(controller); }, ), ), @@ -981,8 +971,8 @@ void main() { await tester.pumpAndSettle(const Duration(seconds: 3)); - final GoogleMapInspector inspector = await inspectorCompleter.future; - final Uint8List? bytes = await inspector.takeSnapshot(); + final GoogleMapController controller = await controllerCompleter.future; + final Uint8List? bytes = await controller.takeSnapshot(); expect(bytes?.isNotEmpty, true); }, // TODO(cyanglaz): un-skip the test when we can test this on CI with API key enabled. @@ -992,15 +982,12 @@ void main() { testWidgets( 'set tileOverlay correctly', (WidgetTester tester) async { - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); final TileOverlay tileOverlay1 = TileOverlay( tileOverlayId: const TileOverlayId('tile_overlay_1'), tileProvider: _DebugTileProvider(), zIndex: 2, - visible: true, transparency: 0.2, - fadeIn: true, ); final TileOverlay tileOverlay2 = TileOverlay( @@ -1018,59 +1005,53 @@ void main() { initialCameraPosition: _kInitialCameraPosition, tileOverlays: {tileOverlay1, tileOverlay2}, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), ), ); await tester.pumpAndSettle(const Duration(seconds: 3)); - final GoogleMapInspector inspector = await inspectorCompleter.future; + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; - final Map tileOverlayInfo1 = - (await inspector.getTileOverlayInfo('tile_overlay_1'))!; - final Map tileOverlayInfo2 = - (await inspector.getTileOverlayInfo('tile_overlay_2'))!; + final TileOverlay tileOverlayInfo1 = (await inspector + .getTileOverlayInfo(tileOverlay1.mapsId, mapId: mapId))!; + final TileOverlay tileOverlayInfo2 = (await inspector + .getTileOverlayInfo(tileOverlay2.mapsId, mapId: mapId))!; - expect(tileOverlayInfo1['visible'], isTrue); - expect(tileOverlayInfo1['fadeIn'], isTrue); - expect(tileOverlayInfo1['transparency'], - moreOrLessEquals(0.2, epsilon: 0.001)); - expect(tileOverlayInfo1['zIndex'], 2); + expect(tileOverlayInfo1.visible, isTrue); + expect(tileOverlayInfo1.fadeIn, isTrue); + expect( + tileOverlayInfo1.transparency, moreOrLessEquals(0.2, epsilon: 0.001)); + expect(tileOverlayInfo1.zIndex, 2); - expect(tileOverlayInfo2['visible'], isFalse); - expect(tileOverlayInfo2['fadeIn'], isFalse); - expect(tileOverlayInfo2['transparency'], - moreOrLessEquals(0.3, epsilon: 0.001)); - expect(tileOverlayInfo2['zIndex'], 1); + expect(tileOverlayInfo2.visible, isFalse); + expect(tileOverlayInfo2.fadeIn, isFalse); + expect( + tileOverlayInfo2.transparency, moreOrLessEquals(0.3, epsilon: 0.001)); + expect(tileOverlayInfo2.zIndex, 1); }, ); testWidgets( 'update tileOverlays correctly', (WidgetTester tester) async { - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); final Key key = GlobalKey(); final TileOverlay tileOverlay1 = TileOverlay( tileOverlayId: const TileOverlayId('tile_overlay_1'), tileProvider: _DebugTileProvider(), zIndex: 2, - visible: true, transparency: 0.2, - fadeIn: true, ); final TileOverlay tileOverlay2 = TileOverlay( tileOverlayId: const TileOverlayId('tile_overlay_2'), tileProvider: _DebugTileProvider(), zIndex: 3, - visible: true, transparency: 0.5, - fadeIn: true, ); await tester.pumpWidget( Directionality( @@ -1080,16 +1061,15 @@ void main() { initialCameraPosition: _kInitialCameraPosition, tileOverlays: {tileOverlay1, tileOverlay2}, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), ), ); - final GoogleMapInspector inspector = await inspectorCompleter.future; + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; final TileOverlay tileOverlay1New = TileOverlay( tileOverlayId: const TileOverlayId('tile_overlay_1'), @@ -1116,16 +1096,16 @@ void main() { await tester.pumpAndSettle(const Duration(seconds: 3)); - final Map tileOverlayInfo1 = - (await inspector.getTileOverlayInfo('tile_overlay_1'))!; - final Map? tileOverlayInfo2 = - await inspector.getTileOverlayInfo('tile_overlay_2'); + final TileOverlay tileOverlayInfo1 = (await inspector + .getTileOverlayInfo(tileOverlay1.mapsId, mapId: mapId))!; + final TileOverlay? tileOverlayInfo2 = + await inspector.getTileOverlayInfo(tileOverlay2.mapsId, mapId: mapId); - expect(tileOverlayInfo1['visible'], isFalse); - expect(tileOverlayInfo1['fadeIn'], isFalse); - expect(tileOverlayInfo1['transparency'], - moreOrLessEquals(0.3, epsilon: 0.001)); - expect(tileOverlayInfo1['zIndex'], 1); + expect(tileOverlayInfo1.visible, isFalse); + expect(tileOverlayInfo1.fadeIn, isFalse); + expect( + tileOverlayInfo1.transparency, moreOrLessEquals(0.3, epsilon: 0.001)); + expect(tileOverlayInfo1.zIndex, 1); expect(tileOverlayInfo2, isNull); }, @@ -1134,16 +1114,13 @@ void main() { testWidgets( 'remove tileOverlays correctly', (WidgetTester tester) async { - final Completer inspectorCompleter = - Completer(); + final Completer mapIdCompleter = Completer(); final Key key = GlobalKey(); final TileOverlay tileOverlay1 = TileOverlay( tileOverlayId: const TileOverlayId('tile_overlay_1'), tileProvider: _DebugTileProvider(), zIndex: 2, - visible: true, transparency: 0.2, - fadeIn: true, ); await tester.pumpWidget( @@ -1154,16 +1131,15 @@ void main() { initialCameraPosition: _kInitialCameraPosition, tileOverlays: {tileOverlay1}, onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel!); - inspectorCompleter.complete(inspector); + mapIdCompleter.complete(controller.mapId); }, ), ), ); - final GoogleMapInspector inspector = await inspectorCompleter.future; + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; await tester.pumpWidget( Directionality( @@ -1179,8 +1155,8 @@ void main() { ); await tester.pumpAndSettle(const Duration(seconds: 3)); - final Map? tileOverlayInfo1 = - await inspector.getTileOverlayInfo('tile_overlay_1'); + final TileOverlay? tileOverlayInfo1 = + await inspector.getTileOverlayInfo(tileOverlay1.mapsId, mapId: mapId); expect(tileOverlayInfo1, isNull); }, @@ -1216,11 +1192,9 @@ class _DebugTileProvider implements TileProvider { textDirection: TextDirection.ltr, ); textPainter.layout( - minWidth: 0.0, maxWidth: width.toDouble(), ); - const Offset offset = Offset(0, 0); - textPainter.paint(canvas, offset); + textPainter.paint(canvas, Offset.zero); canvas.drawRect( Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint); final ui.Picture picture = recorder.endRecording(); diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile b/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile index 9686afaf3c99..8df8fef0a781 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile @@ -29,9 +29,6 @@ flutter_ios_podfile_setup target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end end post_install do |installer| diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj index fbb006aeded0..343e0504134c 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,11 +10,15 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */; }; + 4A097997B7B27CE82FFC3AB8 /* libPods-RunnerUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DC8ED0578E8D540BBDA17645 /* libPods-RunnerUITests.a */; }; + 6851F3562835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */; }; + 68E4726A2836FF0C00BDDDAC /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68E472692836FF0C00BDDDAC /* MapKit.framework */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */ = {isa = PBXBuildFile; fileRef = 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */; }; F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */; }; F7151F21265D7EE50028CB91 /* GoogleMapsUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F20265D7EE50028CB91 /* GoogleMapsUITests.m */; }; FC8F35FC8CD533B128950487 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */; }; @@ -54,6 +58,9 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTGoogleMapJSONConversionsConversionTests.m; sourceTree = ""; }; + 68E472692836FF0C00BDDDAC /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.0.sdk/System/iOSSupport/System/Library/Frameworks/MapKit.framework; sourceTree = DEVELOPER_DIR; }; + 6AC1E6095B09DE4B02ECF64E /* Pods-RunnerUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerUITests/Pods-RunnerUITests.release.xcconfig"; sourceTree = ""; }; 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -67,7 +74,11 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PartiallyMockedMapView.h; sourceTree = ""; }; + 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PartiallyMockedMapView.m; sourceTree = ""; }; B7AFC65E3DD5AC60D834D83D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + DC8ED0578E8D540BBDA17645 /* libPods-RunnerUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + DDDAC1342ABDF2F125577581 /* Pods-RunnerUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerUITests/Pods-RunnerUITests.debug.xcconfig"; sourceTree = ""; }; E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; EA0E91726245EDC22B97E8B9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 68E4726A2836FF0C00BDDDAC /* MapKit.framework in Frameworks */, FC8F35FC8CD533B128950487 /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -100,6 +112,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4A097997B7B27CE82FFC3AB8 /* libPods-RunnerUITests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -109,8 +122,10 @@ 1E7CF0857EFC88FC263CF3B2 /* Frameworks */ = { isa = PBXGroup; children = ( + 68E472692836FF0C00BDDDAC /* MapKit.framework */, 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */, F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */, + DC8ED0578E8D540BBDA17645 /* libPods-RunnerUITests.a */, ); name = Frameworks; sourceTree = ""; @@ -180,6 +195,8 @@ EA0E91726245EDC22B97E8B9 /* Pods-Runner.release.xcconfig */, E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */, 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */, + DDDAC1342ABDF2F125577581 /* Pods-RunnerUITests.debug.xcconfig */, + 6AC1E6095B09DE4B02ECF64E /* Pods-RunnerUITests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -187,7 +204,10 @@ F7151F11265D7ED70028CB91 /* RunnerTests */ = { isa = PBXGroup; children = ( + 6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */, F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */, + 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */, + 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */, F7151F14265D7ED70028CB91 /* Info.plist */, ); path = RunnerTests; @@ -250,6 +270,7 @@ isa = PBXNativeTarget; buildConfigurationList = F7151F25265D7EE50028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; buildPhases = ( + BD39F60794E9A0264D5D3752 /* [CP] Check Pods Manifest.lock */, F7151F1A265D7EE50028CB91 /* Sources */, F7151F1B265D7EE50028CB91 /* Frameworks */, F7151F1C265D7EE50028CB91 /* Resources */, @@ -270,7 +291,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1320; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -401,6 +422,28 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; + BD39F60794E9A0264D5D3752 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; D067548A17DC238B80D2BD12 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -441,6 +484,8 @@ buildActionMask = 2147483647; files = ( F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */, + 6851F3562835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m in Sources */, + 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -511,6 +556,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -567,6 +613,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -643,6 +690,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_FAST_MATH = YES; @@ -658,6 +706,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_FAST_MATH = YES; @@ -669,6 +718,7 @@ }; F7151F26265D7EE50028CB91 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = DDDAC1342ABDF2F125577581 /* Pods-RunnerUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; @@ -682,6 +732,7 @@ }; F7151F27265D7EE50028CB91 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 6AC1E6095B09DE4B02ECF64E /* Pods-RunnerUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index afdb55fdfbdd..c983bfc640ff 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ AnimateCameraState(); } @@ -28,6 +28,7 @@ class AnimateCamera extends StatefulWidget { class AnimateCameraState extends State { GoogleMapController? mapController; + // ignore: use_setters_to_change_properties void _onMapCreated(GoogleMapController controller) { mapController = controller; } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/lite_mode.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/lite_mode.dart index 0ecc5ed38e87..fd95cf864a7c 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/lite_mode.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/lite_mode.dart @@ -5,7 +5,6 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; @@ -13,7 +12,8 @@ const CameraPosition _kInitialPosition = CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); class LiteModePage extends GoogleMapExampleAppPage { - const LiteModePage() : super(const Icon(Icons.map), 'Lite mode'); + const LiteModePage({Key? key}) + : super(const Icon(Icons.map), 'Lite mode', key: key); @override Widget build(BuildContext context) { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart index f4d420a72f7c..60d4fdd95dcf 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart @@ -2,14 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: public_member_api_docs - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:google_maps_flutter_example/lite_mode.dart'; import 'animate_camera.dart'; +import 'lite_mode.dart'; import 'map_click.dart'; import 'map_coordinates.dart'; import 'map_ui.dart'; @@ -43,7 +41,11 @@ final List _allPages = [ const TileOverlayPage(), ]; +/// MapsDemo is the Main Application. class MapsDemo extends StatelessWidget { + /// Default Constructor + const MapsDemo({Key? key}) : super(key: key); + void _pushPage(BuildContext context, GoogleMapExampleAppPage page) { Navigator.of(context).push(MaterialPageRoute( builder: (_) => Scaffold( @@ -69,8 +71,10 @@ class MapsDemo extends StatelessWidget { } void main() { - if (defaultTargetPlatform == TargetPlatform.android) { - AndroidGoogleMapsFlutter.useAndroidViewSurface = true; + final GoogleMapsFlutterPlatform mapsImplementation = + GoogleMapsFlutterPlatform.instance; + if (mapsImplementation is GoogleMapsFlutterAndroid) { + mapsImplementation.useAndroidViewSurface = true; } - runApp(MaterialApp(home: MapsDemo())); + runApp(const MaterialApp(home: MapsDemo())); } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart index ef1bfe2b11dc..ed25d475ebd6 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart @@ -5,7 +5,6 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; @@ -13,7 +12,8 @@ const CameraPosition _kInitialPosition = CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); class MapClickPage extends GoogleMapExampleAppPage { - const MapClickPage() : super(const Icon(Icons.mouse), 'Map click'); + const MapClickPage({Key? key}) + : super(const Icon(Icons.mouse), 'Map click', key: key); @override Widget build(BuildContext context) { @@ -90,7 +90,6 @@ class _MapClickBodyState extends State<_MapClickBody> { ))); } return Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: columnChildren, ); diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart index dc4376a19547..12e31be8f7c7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart @@ -5,7 +5,6 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; @@ -13,7 +12,8 @@ const CameraPosition _kInitialPosition = CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); class MapCoordinatesPage extends GoogleMapExampleAppPage { - const MapCoordinatesPage() : super(const Icon(Icons.map), 'Map coordinates'); + const MapCoordinatesPage({Key? key}) + : super(const Icon(Icons.map), 'Map coordinates', key: key); @override Widget build(BuildContext context) { @@ -42,33 +42,42 @@ class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { final GoogleMap googleMap = GoogleMap( onMapCreated: onMapCreated, initialCameraPosition: _kInitialPosition, + onCameraIdle: + _updateVisibleRegion, // https://github.com/flutter/flutter/issues/54758 ); - final List columnChildren = [ - Padding( - padding: const EdgeInsets.all(10.0), - child: Center( - child: SizedBox( - width: 300.0, - height: 200.0, - child: googleMap, + return NotificationListener( + onNotification: (ScrollNotification scrollState) { + _updateVisibleRegion(); + return true; + }, + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: googleMap, + ), + ), ), - ), + if (mapController != null) + Center( + child: Text('VisibleRegion:' + '\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 + Container( + width: 300, + height: 1000, + ), + ], ), - ]; - - if (mapController != null) { - final String currentVisibleRegion = 'VisibleRegion:' - '\nnortheast: ${_visibleRegion.northeast},' - '\nsouthwest: ${_visibleRegion.southwest}'; - columnChildren.add(Center(child: Text(currentVisibleRegion))); - columnChildren.add(_getVisibleRegionButton()); - } - - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: columnChildren, ); } @@ -80,19 +89,10 @@ class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { }); } - Widget _getVisibleRegionButton() { - return Padding( - padding: const EdgeInsets.all(8.0), - child: ElevatedButton( - child: const Text('Get Visible Region Bounds'), - onPressed: () async { - final LatLngBounds visibleRegion = - await mapController!.getVisibleRegion(); - setState(() { - _visibleRegion = visibleRegion; - }); - }, - ), - ); + Future _updateVisibleRegion() async { + final LatLngBounds visibleRegion = await mapController!.getVisibleRegion(); + setState(() { + _visibleRegion = visibleRegion; + }); } } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart index 48ef1f570e02..3f56f40799af 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart @@ -16,7 +16,8 @@ final LatLngBounds sydneyBounds = LatLngBounds( ); class MapUiPage extends GoogleMapExampleAppPage { - const MapUiPage() : super(const Icon(Icons.map), 'User interface'); + const MapUiPage({Key? key}) + : super(const Icon(Icons.map), 'User interface', key: key); @override Widget build(BuildContext context) { @@ -25,7 +26,7 @@ class MapUiPage extends GoogleMapExampleAppPage { } class MapUiBody extends StatefulWidget { - const MapUiBody(); + const MapUiBody({Key? key}) : super(key: key); @override State createState() => MapUiBodyState(); @@ -335,7 +336,6 @@ class MapUiBodyState extends State { ); } return Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: columnChildren, ); diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart index 95ace9d7c482..58d266c95d1d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart @@ -11,7 +11,8 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; class MarkerIconsPage extends GoogleMapExampleAppPage { - const MarkerIconsPage() : super(const Icon(Icons.image), 'Marker icons'); + const MarkerIconsPage({Key? key}) + : super(const Icon(Icons.image), 'Marker icons', key: key); @override Widget build(BuildContext context) { @@ -20,7 +21,7 @@ class MarkerIconsPage extends GoogleMapExampleAppPage { } class MarkerIconsBody extends StatefulWidget { - const MarkerIconsBody(); + const MarkerIconsBody({Key? key}) : super(key: key); @override State createState() => MarkerIconsBodyState(); diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/move_camera.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/move_camera.dart index 33da90f32c1b..7fa8a0354eb2 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/move_camera.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/move_camera.dart @@ -10,7 +10,8 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; class MoveCameraPage extends GoogleMapExampleAppPage { - const MoveCameraPage() : super(const Icon(Icons.map), 'Camera control'); + const MoveCameraPage({Key? key}) + : super(const Icon(Icons.map), 'Camera control', key: key); @override Widget build(BuildContext context) { @@ -19,7 +20,7 @@ class MoveCameraPage extends GoogleMapExampleAppPage { } class MoveCamera extends StatefulWidget { - const MoveCamera(); + const MoveCamera({Key? key}) : super(key: key); @override State createState() => MoveCameraState(); } @@ -27,6 +28,7 @@ class MoveCamera extends StatefulWidget { class MoveCameraState extends State { GoogleMapController? mapController; + // ignore: use_setters_to_change_properties void _onMapCreated(GoogleMapController controller) { mapController = controller; } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/padding.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/padding.dart index 77091909e970..d5d396fa69c1 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/padding.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/padding.dart @@ -9,7 +9,8 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; class PaddingPage extends GoogleMapExampleAppPage { - const PaddingPage() : super(const Icon(Icons.map), 'Add padding to the map'); + const PaddingPage({Key? key}) + : super(const Icon(Icons.map), 'Add padding to the map', key: key); @override Widget build(BuildContext context) { @@ -18,7 +19,7 @@ class PaddingPage extends GoogleMapExampleAppPage { } class MarkerIconsBody extends StatefulWidget { - const MarkerIconsBody(); + const MarkerIconsBody({Key? key}) : super(key: key); @override State createState() => MarkerIconsBodyState(); @@ -29,7 +30,7 @@ const LatLng _kMapCenter = LatLng(52.4478, -3.5402); class MarkerIconsBodyState extends State { GoogleMapController? controller; - EdgeInsets _padding = const EdgeInsets.all(0); + EdgeInsets _padding = EdgeInsets.zero; @override Widget build(BuildContext context) { @@ -67,7 +68,6 @@ class MarkerIconsBodyState extends State { columnChildren.addAll([_paddingInput(), _buttons()]); return Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: columnChildren, ); @@ -167,7 +167,7 @@ class MarkerIconsBodyState extends State { _bottomController.clear(); _leftController.clear(); _rightController.clear(); - _padding = const EdgeInsets.all(0); + _padding = EdgeInsets.zero; }); }, ) diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/page.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/page.dart index fb6eb3260f6d..eb01ab07a6f3 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/page.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/page.dart @@ -7,7 +7,8 @@ import 'package:flutter/material.dart'; abstract class GoogleMapExampleAppPage extends StatelessWidget { - const GoogleMapExampleAppPage(this.leading, this.title); + const GoogleMapExampleAppPage(this.leading, this.title, {Key? key}) + : super(key: key); final Widget leading; final String title; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_circle.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_circle.dart index c6f1509af69f..7cbb63ac4e99 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_circle.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_circle.dart @@ -10,8 +10,8 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; class PlaceCirclePage extends GoogleMapExampleAppPage { - const PlaceCirclePage() - : super(const Icon(Icons.linear_scale), 'Place circle'); + const PlaceCirclePage({Key? key}) + : super(const Icon(Icons.linear_scale), 'Place circle', key: key); @override Widget build(BuildContext context) { @@ -20,7 +20,7 @@ class PlaceCirclePage extends GoogleMapExampleAppPage { } class PlaceCircleBody extends StatefulWidget { - const PlaceCircleBody(); + const PlaceCircleBody({Key? key}) : super(key: key); @override State createState() => PlaceCircleBodyState(); @@ -48,6 +48,7 @@ class PlaceCircleBodyState extends State { int widthsIndex = 0; List widths = [10, 20, 5]; + // ignore: use_setters_to_change_properties void _onMapCreated(GoogleMapController controller) { this.controller = controller; } @@ -170,42 +171,42 @@ class PlaceCircleBodyState extends State { Column( children: [ TextButton( - child: const Text('add'), onPressed: _add, + child: const Text('add'), ), TextButton( - child: const Text('remove'), onPressed: (selectedId == null) ? null : () => _remove(selectedId), + child: const Text('remove'), ), TextButton( - child: const Text('toggle visible'), onPressed: (selectedId == null) ? null : () => _toggleVisible(selectedId), + child: const Text('toggle visible'), ), ], ), Column( children: [ TextButton( - child: const Text('change stroke width'), onPressed: (selectedId == null) ? null : () => _changeStrokeWidth(selectedId), + child: const Text('change stroke width'), ), TextButton( - child: const Text('change stroke color'), onPressed: (selectedId == null) ? null : () => _changeStrokeColor(selectedId), + child: const Text('change stroke color'), ), TextButton( - child: const Text('change fill color'), onPressed: (selectedId == null) ? null : () => _changeFillColor(selectedId), + child: const Text('change fill color'), ), ], ) diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart index 4291cac6841e..fa49917e715f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart @@ -15,7 +15,8 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; class PlaceMarkerPage extends GoogleMapExampleAppPage { - const PlaceMarkerPage() : super(const Icon(Icons.place), 'Place marker'); + const PlaceMarkerPage({Key? key}) + : super(const Icon(Icons.place), 'Place marker', key: key); @override Widget build(BuildContext context) { @@ -24,7 +25,7 @@ class PlaceMarkerPage extends GoogleMapExampleAppPage { } class PlaceMarkerBody extends StatefulWidget { - const PlaceMarkerBody(); + const PlaceMarkerBody({Key? key}) : super(key: key); @override State createState() => PlaceMarkerBodyState(); @@ -42,6 +43,7 @@ class PlaceMarkerBodyState extends State { int _markerIdCounter = 1; LatLng? markerPosition; + // ignore: use_setters_to_change_properties void _onMapCreated(GoogleMapController controller) { this.controller = controller; } @@ -206,7 +208,7 @@ class PlaceMarkerBodyState extends State { Future _changeInfo(MarkerId markerId) async { final Marker marker = markers[markerId]!; - final String newSnippet = marker.infoWindow.snippet! + '*'; + final String newSnippet = '${marker.infoWindow.snippet!}*'; setState(() { markers[markerId] = marker.copyWith( infoWindowParam: marker.infoWindow.copyWith( @@ -308,13 +310,13 @@ class PlaceMarkerBodyState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ TextButton( - child: const Text('Add'), onPressed: _add, + child: const Text('Add'), ), TextButton( - child: const Text('Remove'), onPressed: selectedId == null ? null : () => _remove(selectedId), + child: const Text('Remove'), ), ], ), @@ -322,62 +324,61 @@ class PlaceMarkerBodyState extends State { alignment: WrapAlignment.spaceEvenly, children: [ TextButton( - child: const Text('change info'), onPressed: selectedId == null ? null : () => _changeInfo(selectedId), + child: const Text('change info'), ), TextButton( - child: const Text('change info anchor'), onPressed: selectedId == null ? null : () => _changeInfoAnchor(selectedId), + child: const Text('change info anchor'), ), TextButton( - child: const Text('change alpha'), onPressed: selectedId == null ? null : () => _changeAlpha(selectedId), + child: const Text('change alpha'), ), TextButton( - child: const Text('change anchor'), onPressed: selectedId == null ? null : () => _changeAnchor(selectedId), + child: const Text('change anchor'), ), TextButton( - child: const Text('toggle draggable'), onPressed: selectedId == null ? null : () => _toggleDraggable(selectedId), + child: const Text('toggle draggable'), ), TextButton( - child: const Text('toggle flat'), onPressed: selectedId == null ? null : () => _toggleFlat(selectedId), + child: const Text('toggle flat'), ), TextButton( - child: const Text('change position'), onPressed: selectedId == null ? null : () => _changePosition(selectedId), + child: const Text('change position'), ), TextButton( - child: const Text('change rotation'), onPressed: selectedId == null ? null : () => _changeRotation(selectedId), + child: const Text('change rotation'), ), TextButton( - child: const Text('toggle visible'), onPressed: selectedId == null ? null : () => _toggleVisible(selectedId), + child: const Text('toggle visible'), ), TextButton( - child: const Text('change zIndex'), onPressed: selectedId == null ? null : () => _changeZIndex(selectedId), + child: const Text('change zIndex'), ), TextButton( - child: const Text('set marker icon'), onPressed: selectedId == null ? null : () { @@ -387,6 +388,7 @@ class PlaceMarkerBodyState extends State { }, ); }, + child: const Text('set marker icon'), ), ], ), @@ -400,7 +402,6 @@ class PlaceMarkerBodyState extends State { padding: const EdgeInsets.only(left: 12, right: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.max, children: [ if (markerPosition == null) Container() diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polygon.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polygon.dart index 8d4fa3e07c36..cb0cc56d4754 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polygon.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polygon.dart @@ -10,8 +10,8 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; class PlacePolygonPage extends GoogleMapExampleAppPage { - const PlacePolygonPage() - : super(const Icon(Icons.linear_scale), 'Place polygon'); + const PlacePolygonPage({Key? key}) + : super(const Icon(Icons.linear_scale), 'Place polygon', key: key); @override Widget build(BuildContext context) { @@ -20,7 +20,7 @@ class PlacePolygonPage extends GoogleMapExampleAppPage { } class PlacePolygonBody extends StatefulWidget { - const PlacePolygonBody(); + const PlacePolygonBody({Key? key}) : super(key: key); @override State createState() => PlacePolygonBodyState(); @@ -49,6 +49,7 @@ class PlacePolygonBodyState extends State { int widthsIndex = 0; List widths = [10, 20, 5]; + // ignore: use_setters_to_change_properties void _onMapCreated(GoogleMapController controller) { this.controller = controller; } @@ -196,64 +197,64 @@ class PlacePolygonBodyState extends State { Column( children: [ TextButton( - child: const Text('add'), onPressed: _add, + child: const Text('add'), ), TextButton( - child: const Text('remove'), onPressed: (selectedId == null) ? null : () => _remove(selectedId), + child: const Text('remove'), ), TextButton( - child: const Text('toggle visible'), onPressed: (selectedId == null) ? null : () => _toggleVisible(selectedId), + child: const Text('toggle visible'), ), TextButton( - child: const Text('toggle geodesic'), onPressed: (selectedId == null) ? null : () => _toggleGeodesic(selectedId), + child: const Text('toggle geodesic'), ), ], ), Column( children: [ TextButton( - child: const Text('add holes'), onPressed: (selectedId == null) ? null : ((polygons[selectedId]!.holes.isNotEmpty) ? null : () => _addHoles(selectedId)), + child: const Text('add holes'), ), TextButton( - child: const Text('remove holes'), onPressed: (selectedId == null) ? null : ((polygons[selectedId]!.holes.isEmpty) ? null : () => _removeHoles(selectedId)), + child: const Text('remove holes'), ), TextButton( - child: const Text('change stroke width'), onPressed: (selectedId == null) ? null : () => _changeWidth(selectedId), + child: const Text('change stroke width'), ), TextButton( - child: const Text('change stroke color'), onPressed: (selectedId == null) ? null : () => _changeStrokeColor(selectedId), + child: const Text('change stroke color'), ), TextButton( - child: const Text('change fill color'), onPressed: (selectedId == null) ? null : () => _changeFillColor(selectedId), + child: const Text('change fill color'), ), ], ) diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polyline.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polyline.dart index 434920d293be..7a7c5d2f4a16 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polyline.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polyline.dart @@ -11,8 +11,8 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; class PlacePolylinePage extends GoogleMapExampleAppPage { - const PlacePolylinePage() - : super(const Icon(Icons.linear_scale), 'Place polyline'); + const PlacePolylinePage({Key? key}) + : super(const Icon(Icons.linear_scale), 'Place polyline', key: key); @override Widget build(BuildContext context) { @@ -21,7 +21,7 @@ class PlacePolylinePage extends GoogleMapExampleAppPage { } class PlacePolylineBody extends StatefulWidget { - const PlacePolylineBody(); + const PlacePolylineBody({Key? key}) : super(key: key); @override State createState() => PlacePolylineBodyState(); @@ -77,6 +77,7 @@ class PlacePolylineBodyState extends State { [PatternItem.dot, PatternItem.gap(10.0)], ]; + // ignore: use_setters_to_change_properties void _onMapCreated(GoogleMapController controller) { this.controller = controller; } @@ -234,66 +235,66 @@ class PlacePolylineBodyState extends State { Column( children: [ TextButton( - child: const Text('add'), onPressed: _add, + child: const Text('add'), ), TextButton( - child: const Text('remove'), onPressed: (selectedId == null) ? null : () => _remove(selectedId), + child: const Text('remove'), ), TextButton( - child: const Text('toggle visible'), onPressed: (selectedId == null) ? null : () => _toggleVisible(selectedId), + child: const Text('toggle visible'), ), TextButton( - child: const Text('toggle geodesic'), onPressed: (selectedId == null) ? null : () => _toggleGeodesic(selectedId), + child: const Text('toggle geodesic'), ), ], ), Column( children: [ TextButton( - child: const Text('change width'), onPressed: (selectedId == null) ? null : () => _changeWidth(selectedId), + child: const Text('change width'), ), TextButton( - child: const Text('change color'), onPressed: (selectedId == null) ? null : () => _changeColor(selectedId), + child: const Text('change color'), ), TextButton( - child: const Text('change start cap [Android only]'), onPressed: isIOS || (selectedId == null) ? null : () => _changeStartCap(selectedId), + child: const Text('change start cap [Android only]'), ), TextButton( - child: const Text('change end cap [Android only]'), onPressed: isIOS || (selectedId == null) ? null : () => _changeEndCap(selectedId), + child: const Text('change end cap [Android only]'), ), TextButton( - child: const Text('change joint type [Android only]'), onPressed: isIOS || (selectedId == null) ? null : () => _changeJointType(selectedId), + child: const Text('change joint type [Android only]'), ), TextButton( - child: const Text('change pattern [Android only]'), onPressed: isIOS || (selectedId == null) ? null : () => _changePattern(selectedId), + child: const Text('change pattern [Android only]'), ), ], ) diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart index 8d046fc6b387..3d676e0713fd 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart @@ -7,7 +7,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; @@ -15,7 +14,8 @@ import 'page.dart'; const LatLng _center = LatLng(32.080664, 34.9563837); class ScrollingMapPage extends GoogleMapExampleAppPage { - const ScrollingMapPage() : super(const Icon(Icons.map), 'Scrolling map'); + const ScrollingMapPage({Key? key}) + : super(const Icon(Icons.map), 'Scrolling map', key: key); @override Widget build(BuildContext context) { @@ -24,7 +24,7 @@ class ScrollingMapPage extends GoogleMapExampleAppPage { } class ScrollingMapBody extends StatelessWidget { - const ScrollingMapBody(); + const ScrollingMapBody({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -66,7 +66,7 @@ class ScrollingMapBody extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 30.0), child: Column( children: [ - const Text('This map doesn\'t consume the vertical drags.'), + const Text("This map doesn't consume the vertical drags."), const Padding( padding: EdgeInsets.only(bottom: 12.0), child: diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/snapshot.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/snapshot.dart index 9afc28869490..fbc7ae2a3e24 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/snapshot.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/snapshot.dart @@ -15,8 +15,9 @@ const CameraPosition _kInitialPosition = CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); class SnapshotPage extends GoogleMapExampleAppPage { - const SnapshotPage() - : super(const Icon(Icons.camera_alt), 'Take a snapshot of the map'); + const SnapshotPage({Key? key}) + : super(const Icon(Icons.camera_alt), 'Take a snapshot of the map', + key: key); @override Widget build(BuildContext context) { @@ -67,6 +68,7 @@ class _SnapshotBodyState extends State<_SnapshotBody> { ); } + // ignore: use_setters_to_change_properties void onMapCreated(GoogleMapController controller) { _mapController = controller; } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart index dc7f7d1d6f33..31f470dd9c25 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart @@ -13,7 +13,8 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; class TileOverlayPage extends GoogleMapExampleAppPage { - const TileOverlayPage() : super(const Icon(Icons.map), 'Tile overlay'); + const TileOverlayPage({Key? key}) + : super(const Icon(Icons.map), 'Tile overlay', key: key); @override Widget build(BuildContext context) { @@ -22,7 +23,7 @@ class TileOverlayPage extends GoogleMapExampleAppPage { } class TileOverlayBody extends StatefulWidget { - const TileOverlayBody(); + const TileOverlayBody({Key? key}) : super(key: key); @override State createState() => TileOverlayBodyState(); @@ -34,6 +35,7 @@ class TileOverlayBodyState extends State { GoogleMapController? controller; TileOverlay? _tileOverlay; + // ignore: use_setters_to_change_properties void _onMapCreated(GoogleMapController controller) { this.controller = controller; } @@ -90,16 +92,16 @@ class TileOverlayBodyState extends State { ), ), TextButton( - child: const Text('Add tile overlay'), onPressed: _addTileOverlay, + child: const Text('Add tile overlay'), ), TextButton( - child: const Text('Remove tile overlay'), onPressed: _removeTileOverlay, + child: const Text('Remove tile overlay'), ), TextButton( - child: const Text('Clear tile cache'), onPressed: _clearTileCache, + child: const Text('Clear tile cache'), ), ], ); @@ -135,11 +137,9 @@ class _DebugTileProvider implements TileProvider { textDirection: TextDirection.ltr, ); textPainter.layout( - minWidth: 0.0, maxWidth: width.toDouble(), ); - const Offset offset = Offset(0, 0); - textPainter.paint(canvas, offset); + textPainter.paint(canvas, Offset.zero); canvas.drawRect( Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint); final ui.Picture picture = recorder.endRecording(); 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 dbc32b0ef2f8..b86f05f3360a 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.5.0" + flutter: ">=2.10.0" dependencies: cupertino_icons: ^0.1.0 @@ -18,11 +18,15 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ + google_maps_flutter_android: ^2.1.10 + google_maps_flutter_platform_interface: ^2.2.1 dev_dependencies: - espresso: ^0.1.0+2 + espresso: ^0.2.0 flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h deleted file mode 100644 index 356a13faba62..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h +++ /dev/null @@ -1,42 +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 -#import - -NS_ASSUME_NONNULL_BEGIN - -// Defines map UI options writable from Flutter. -@protocol FLTGoogleMapTileOverlayOptionsSink -- (void)setFadeIn:(BOOL)fadeIn; -- (void)setTransparency:(float)transparency; -- (void)setZIndex:(int)zIndex; -- (void)setVisible:(BOOL)visible; -- (void)setTileSize:(NSInteger)tileSize; -@end - -@interface FLTGoogleMapTileOverlayController : NSObject -- (instancetype)initWithTileLayer:(GMSTileLayer *)tileLayer mapView:(GMSMapView *)mapView; -- (void)removeTileOverlay; -- (void)clearTileCache; -- (NSDictionary *)getTileOverlayInfo; -@end - -@interface FLTTileProviderController : GMSTileLayer -@property(copy, nonatomic, readonly) NSString *tileOverlayId; -- (instancetype)init:(FlutterMethodChannel *)methodChannel tileOverlayId:(NSString *)tileOverlayId; -@end - -@interface FLTTileOverlaysController : NSObject -- (instancetype)init:(FlutterMethodChannel *)methodChannel - mapView:(GMSMapView *)mapView - registrar:(NSObject *)registrar; -- (void)addTileOverlays:(NSArray *)tileOverlaysToAdd; -- (void)changeTileOverlays:(NSArray *)tileOverlaysToChange; -- (void)removeTileOverlayIds:(NSArray *)tileOverlayIdsToRemove; -- (void)clearTileCache:(NSString *)tileOverlayId; -- (nullable NSDictionary *)getTileOverlayInfo:(NSString *)tileverlayId; -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.h deleted file mode 100644 index 26b7ce573bdf..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.h +++ /dev/null @@ -1,39 +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 -#import - -// Defines circle UI options writable from Flutter. -@protocol FLTGoogleMapCircleOptionsSink -- (void)setConsumeTapEvents:(BOOL)consume; -- (void)setVisible:(BOOL)visible; -- (void)setStrokeColor:(UIColor *)color; -- (void)setStrokeWidth:(CGFloat)width; -- (void)setFillColor:(UIColor *)color; -- (void)setCenter:(CLLocationCoordinate2D)center; -- (void)setRadius:(CLLocationDistance)radius; -- (void)setZIndex:(int)zIndex; -@end - -// Defines circle controllable by Flutter. -@interface FLTGoogleMapCircleController : NSObject -@property(atomic, readonly) NSString *circleId; -- (instancetype)initCircleWithPosition:(CLLocationCoordinate2D)position - radius:(CLLocationDistance)radius - circleId:(NSString *)circleId - mapView:(GMSMapView *)mapView; -- (void)removeCircle; -@end - -@interface FLTCirclesController : NSObject -- (instancetype)init:(FlutterMethodChannel *)methodChannel - mapView:(GMSMapView *)mapView - registrar:(NSObject *)registrar; -- (void)addCircles:(NSArray *)circlesToAdd; -- (void)changeCircles:(NSArray *)circlesToChange; -- (void)removeCircleIds:(NSArray *)circleIdsToRemove; -- (void)onCircleTap:(NSString *)circleId; -- (bool)hasCircleWithId:(NSString *)circleId; -@end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.m deleted file mode 100644 index d97de587fb17..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.m +++ /dev/null @@ -1,198 +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 "GoogleMapCircleController.h" -#import "JsonConversions.h" - -@implementation FLTGoogleMapCircleController { - GMSCircle *_circle; - GMSMapView *_mapView; -} -- (instancetype)initCircleWithPosition:(CLLocationCoordinate2D)position - radius:(CLLocationDistance)radius - circleId:(NSString *)circleId - mapView:(GMSMapView *)mapView { - self = [super init]; - if (self) { - _circle = [GMSCircle circleWithPosition:position radius:radius]; - _mapView = mapView; - _circleId = circleId; - _circle.userData = @[ circleId ]; - } - return self; -} - -- (void)removeCircle { - _circle.map = nil; -} - -#pragma mark - FLTGoogleMapCircleOptionsSink methods - -- (void)setConsumeTapEvents:(BOOL)consumes { - _circle.tappable = consumes; -} -- (void)setVisible:(BOOL)visible { - _circle.map = visible ? _mapView : nil; -} -- (void)setZIndex:(int)zIndex { - _circle.zIndex = zIndex; -} -- (void)setCenter:(CLLocationCoordinate2D)center { - _circle.position = center; -} -- (void)setRadius:(CLLocationDistance)radius { - _circle.radius = radius; -} - -- (void)setStrokeColor:(UIColor *)color { - _circle.strokeColor = color; -} -- (void)setStrokeWidth:(CGFloat)width { - _circle.strokeWidth = width; -} -- (void)setFillColor:(UIColor *)color { - _circle.fillColor = color; -} -@end - -static int ToInt(NSNumber *data) { return [FLTGoogleMapJsonConversions toInt:data]; } - -static BOOL ToBool(NSNumber *data) { return [FLTGoogleMapJsonConversions toBool:data]; } - -static CLLocationCoordinate2D ToLocation(NSArray *data) { - return [FLTGoogleMapJsonConversions toLocation:data]; -} - -static CLLocationDistance ToDistance(NSNumber *data) { - return [FLTGoogleMapJsonConversions toFloat:data]; -} - -static UIColor *ToColor(NSNumber *data) { return [FLTGoogleMapJsonConversions toColor:data]; } - -static void InterpretCircleOptions(NSDictionary *data, id sink, - NSObject *registrar) { - NSNumber *consumeTapEvents = data[@"consumeTapEvents"]; - if (consumeTapEvents != nil) { - [sink setConsumeTapEvents:ToBool(consumeTapEvents)]; - } - - NSNumber *visible = data[@"visible"]; - if (visible != nil) { - [sink setVisible:ToBool(visible)]; - } - - NSNumber *zIndex = data[@"zIndex"]; - if (zIndex != nil) { - [sink setZIndex:ToInt(zIndex)]; - } - - NSArray *center = data[@"center"]; - if (center) { - [sink setCenter:ToLocation(center)]; - } - - NSNumber *radius = data[@"radius"]; - if (radius != nil) { - [sink setRadius:ToDistance(radius)]; - } - - NSNumber *strokeColor = data[@"strokeColor"]; - if (strokeColor != nil) { - [sink setStrokeColor:ToColor(strokeColor)]; - } - - NSNumber *strokeWidth = data[@"strokeWidth"]; - if (strokeWidth != nil) { - [sink setStrokeWidth:ToInt(strokeWidth)]; - } - - NSNumber *fillColor = data[@"fillColor"]; - if (fillColor != nil) { - [sink setFillColor:ToColor(fillColor)]; - } -} - -@implementation FLTCirclesController { - NSMutableDictionary *_circleIdToController; - FlutterMethodChannel *_methodChannel; - NSObject *_registrar; - GMSMapView *_mapView; -} -- (instancetype)init:(FlutterMethodChannel *)methodChannel - mapView:(GMSMapView *)mapView - registrar:(NSObject *)registrar { - self = [super init]; - if (self) { - _methodChannel = methodChannel; - _mapView = mapView; - _circleIdToController = [NSMutableDictionary dictionaryWithCapacity:1]; - _registrar = registrar; - } - return self; -} -- (void)addCircles:(NSArray *)circlesToAdd { - for (NSDictionary *circle in circlesToAdd) { - CLLocationCoordinate2D position = [FLTCirclesController getPosition:circle]; - CLLocationDistance radius = [FLTCirclesController getRadius:circle]; - NSString *circleId = [FLTCirclesController getCircleId:circle]; - FLTGoogleMapCircleController *controller = - [[FLTGoogleMapCircleController alloc] initCircleWithPosition:position - radius:radius - circleId:circleId - mapView:_mapView]; - InterpretCircleOptions(circle, controller, _registrar); - _circleIdToController[circleId] = controller; - } -} -- (void)changeCircles:(NSArray *)circlesToChange { - for (NSDictionary *circle in circlesToChange) { - NSString *circleId = [FLTCirclesController getCircleId:circle]; - FLTGoogleMapCircleController *controller = _circleIdToController[circleId]; - if (!controller) { - continue; - } - InterpretCircleOptions(circle, controller, _registrar); - } -} -- (void)removeCircleIds:(NSArray *)circleIdsToRemove { - for (NSString *circleId in circleIdsToRemove) { - if (!circleId) { - continue; - } - FLTGoogleMapCircleController *controller = _circleIdToController[circleId]; - if (!controller) { - continue; - } - [controller removeCircle]; - [_circleIdToController removeObjectForKey:circleId]; - } -} -- (bool)hasCircleWithId:(NSString *)circleId { - if (!circleId) { - return false; - } - return _circleIdToController[circleId] != nil; -} -- (void)onCircleTap:(NSString *)circleId { - if (!circleId) { - return; - } - FLTGoogleMapCircleController *controller = _circleIdToController[circleId]; - if (!controller) { - return; - } - [_methodChannel invokeMethod:@"circle#onTap" arguments:@{@"circleId" : circleId}]; -} -+ (CLLocationCoordinate2D)getPosition:(NSDictionary *)circle { - NSArray *center = circle[@"center"]; - return ToLocation(center); -} -+ (CLLocationDistance)getRadius:(NSDictionary *)circle { - NSNumber *radius = circle[@"radius"]; - return ToDistance(radius); -} -+ (NSString *)getCircleId:(NSDictionary *)circle { - return circle[@"circleId"]; -} -@end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h deleted file mode 100644 index 8734c06fe929..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h +++ /dev/null @@ -1,57 +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 -#import -#import "GoogleMapController.h" - -NS_ASSUME_NONNULL_BEGIN - -// Defines marker UI options writable from Flutter. -@protocol FLTGoogleMapMarkerOptionsSink -- (void)setAlpha:(float)alpha; -- (void)setAnchor:(CGPoint)anchor; -- (void)setConsumeTapEvents:(BOOL)consume; -- (void)setDraggable:(BOOL)draggable; -- (void)setFlat:(BOOL)flat; -- (void)setIcon:(UIImage *)icon; -- (void)setInfoWindowAnchor:(CGPoint)anchor; -- (void)setInfoWindowTitle:(NSString *)title snippet:(NSString *)snippet; -- (void)setPosition:(CLLocationCoordinate2D)position; -- (void)setRotation:(CLLocationDegrees)rotation; -- (void)setVisible:(BOOL)visible; -- (void)setZIndex:(int)zIndex; -@end - -// Defines marker controllable by Flutter. -@interface FLTGoogleMapMarkerController : NSObject -@property(atomic, readonly) NSString *markerId; -- (instancetype)initMarkerWithPosition:(CLLocationCoordinate2D)position - markerId:(NSString *)markerId - mapView:(GMSMapView *)mapView; -- (void)showInfoWindow; -- (void)hideInfoWindow; -- (BOOL)isInfoWindowShown; -- (BOOL)consumeTapEvents; -- (void)removeMarker; -@end - -@interface FLTMarkersController : NSObject -- (instancetype)init:(FlutterMethodChannel *)methodChannel - mapView:(GMSMapView *)mapView - registrar:(NSObject *)registrar; -- (void)addMarkers:(NSArray *)markersToAdd; -- (void)changeMarkers:(NSArray *)markersToChange; -- (void)removeMarkerIds:(NSArray *)markerIdsToRemove; -- (BOOL)onMarkerTap:(NSString *)markerId; -- (void)onMarkerDragStart:(NSString *)markerId coordinate:(CLLocationCoordinate2D)coordinate; -- (void)onMarkerDragEnd:(NSString *)markerId coordinate:(CLLocationCoordinate2D)coordinate; -- (void)onMarkerDrag:(NSString *)markerId coordinate:(CLLocationCoordinate2D)coordinate; -- (void)onInfoWindowTap:(NSString *)markerId; -- (void)showMarkerInfoWindow:(NSString *)markerId result:(FlutterResult)result; -- (void)hideMarkerInfoWindow:(NSString *)markerId result:(FlutterResult)result; -- (void)isMarkerInfoWindowShown:(NSString *)markerId result:(FlutterResult)result; -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m deleted file mode 100644 index c2877e2bd78f..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m +++ /dev/null @@ -1,376 +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 "GoogleMapMarkerController.h" -#import "JsonConversions.h" - -static UIImage *ExtractIcon(NSObject *registrar, NSArray *icon); -static void InterpretInfoWindow(id sink, NSDictionary *data); - -@implementation FLTGoogleMapMarkerController { - GMSMarker *_marker; - GMSMapView *_mapView; - BOOL _consumeTapEvents; -} -- (instancetype)initMarkerWithPosition:(CLLocationCoordinate2D)position - markerId:(NSString *)markerId - mapView:(GMSMapView *)mapView { - self = [super init]; - if (self) { - _marker = [GMSMarker markerWithPosition:position]; - _mapView = mapView; - _markerId = markerId; - _marker.userData = @[ _markerId ]; - _consumeTapEvents = NO; - } - return self; -} -- (void)showInfoWindow { - _mapView.selectedMarker = _marker; -} -- (void)hideInfoWindow { - if (_mapView.selectedMarker == _marker) { - _mapView.selectedMarker = nil; - } -} -- (BOOL)isInfoWindowShown { - return _mapView.selectedMarker == _marker; -} -- (BOOL)consumeTapEvents { - return _consumeTapEvents; -} -- (void)removeMarker { - _marker.map = nil; -} - -#pragma mark - FLTGoogleMapMarkerOptionsSink methods - -- (void)setAlpha:(float)alpha { - _marker.opacity = alpha; -} -- (void)setAnchor:(CGPoint)anchor { - _marker.groundAnchor = anchor; -} -- (void)setConsumeTapEvents:(BOOL)consumes { - _consumeTapEvents = consumes; -} -- (void)setDraggable:(BOOL)draggable { - _marker.draggable = draggable; -} -- (void)setFlat:(BOOL)flat { - _marker.flat = flat; -} -- (void)setIcon:(UIImage *)icon { - _marker.icon = icon; -} -- (void)setInfoWindowAnchor:(CGPoint)anchor { - _marker.infoWindowAnchor = anchor; -} -- (void)setInfoWindowTitle:(NSString *)title snippet:(NSString *)snippet { - _marker.title = title; - _marker.snippet = snippet; -} -- (void)setPosition:(CLLocationCoordinate2D)position { - _marker.position = position; -} -- (void)setRotation:(CLLocationDegrees)rotation { - _marker.rotation = rotation; -} -- (void)setVisible:(BOOL)visible { - _marker.map = visible ? _mapView : nil; -} -- (void)setZIndex:(int)zIndex { - _marker.zIndex = zIndex; -} -@end - -static double ToDouble(NSNumber *data) { return [FLTGoogleMapJsonConversions toDouble:data]; } - -static float ToFloat(NSNumber *data) { return [FLTGoogleMapJsonConversions toFloat:data]; } - -static CLLocationCoordinate2D ToLocation(NSArray *data) { - return [FLTGoogleMapJsonConversions toLocation:data]; -} - -static int ToInt(NSNumber *data) { return [FLTGoogleMapJsonConversions toInt:data]; } - -static BOOL ToBool(NSNumber *data) { return [FLTGoogleMapJsonConversions toBool:data]; } - -static CGPoint ToPoint(NSArray *data) { return [FLTGoogleMapJsonConversions toPoint:data]; } - -static NSArray *PositionToJson(CLLocationCoordinate2D data) { - return [FLTGoogleMapJsonConversions positionToJson:data]; -} - -static void InterpretMarkerOptions(NSDictionary *data, id sink, - NSObject *registrar) { - NSNumber *alpha = data[@"alpha"]; - if (alpha != nil) { - [sink setAlpha:ToFloat(alpha)]; - } - NSArray *anchor = data[@"anchor"]; - if (anchor) { - [sink setAnchor:ToPoint(anchor)]; - } - NSNumber *draggable = data[@"draggable"]; - if (draggable != nil) { - [sink setDraggable:ToBool(draggable)]; - } - NSArray *icon = data[@"icon"]; - if (icon) { - UIImage *image = ExtractIcon(registrar, icon); - [sink setIcon:image]; - } - NSNumber *flat = data[@"flat"]; - if (flat != nil) { - [sink setFlat:ToBool(flat)]; - } - NSNumber *consumeTapEvents = data[@"consumeTapEvents"]; - if (consumeTapEvents != nil) { - [sink setConsumeTapEvents:ToBool(consumeTapEvents)]; - } - InterpretInfoWindow(sink, data); - NSArray *position = data[@"position"]; - if (position) { - [sink setPosition:ToLocation(position)]; - } - NSNumber *rotation = data[@"rotation"]; - if (rotation != nil) { - [sink setRotation:ToDouble(rotation)]; - } - NSNumber *visible = data[@"visible"]; - if (visible != nil) { - [sink setVisible:ToBool(visible)]; - } - NSNumber *zIndex = data[@"zIndex"]; - if (zIndex != nil) { - [sink setZIndex:ToInt(zIndex)]; - } -} - -static void InterpretInfoWindow(id sink, NSDictionary *data) { - NSDictionary *infoWindow = data[@"infoWindow"]; - if (infoWindow) { - NSString *title = infoWindow[@"title"]; - NSString *snippet = infoWindow[@"snippet"]; - if (title) { - [sink setInfoWindowTitle:title snippet:snippet]; - } - NSArray *infoWindowAnchor = infoWindow[@"infoWindowAnchor"]; - if (infoWindowAnchor) { - [sink setInfoWindowAnchor:ToPoint(infoWindowAnchor)]; - } - } -} - -static UIImage *scaleImage(UIImage *image, NSNumber *scaleParam) { - double scale = 1.0; - if ([scaleParam isKindOfClass:[NSNumber class]]) { - scale = scaleParam.doubleValue; - } - if (fabs(scale - 1) > 1e-3) { - return [UIImage imageWithCGImage:[image CGImage] - scale:(image.scale * scale) - orientation:(image.imageOrientation)]; - } - return image; -} - -static UIImage *ExtractIcon(NSObject *registrar, NSArray *iconData) { - UIImage *image; - if ([iconData.firstObject isEqualToString:@"defaultMarker"]) { - CGFloat hue = (iconData.count == 1) ? 0.0f : ToDouble(iconData[1]); - image = [GMSMarker markerImageWithColor:[UIColor colorWithHue:hue / 360.0 - saturation:1.0 - brightness:0.7 - alpha:1.0]]; - } else if ([iconData.firstObject isEqualToString:@"fromAsset"]) { - if (iconData.count == 2) { - image = [UIImage imageNamed:[registrar lookupKeyForAsset:iconData[1]]]; - } else { - image = [UIImage imageNamed:[registrar lookupKeyForAsset:iconData[1] - fromPackage:iconData[2]]]; - } - } else if ([iconData.firstObject isEqualToString:@"fromAssetImage"]) { - if (iconData.count == 3) { - image = [UIImage imageNamed:[registrar lookupKeyForAsset:iconData[1]]]; - NSNumber *scaleParam = iconData[2]; - image = scaleImage(image, scaleParam); - } else { - NSString *error = - [NSString stringWithFormat:@"'fromAssetImage' should have exactly 3 arguments. Got: %lu", - (unsigned long)iconData.count]; - NSException *exception = [NSException exceptionWithName:@"InvalidBitmapDescriptor" - reason:error - userInfo:nil]; - @throw exception; - } - } else if ([iconData[0] isEqualToString:@"fromBytes"]) { - if (iconData.count == 2) { - @try { - FlutterStandardTypedData *byteData = iconData[1]; - CGFloat screenScale = [[UIScreen mainScreen] scale]; - image = [UIImage imageWithData:[byteData data] scale:screenScale]; - } @catch (NSException *exception) { - @throw [NSException exceptionWithName:@"InvalidByteDescriptor" - reason:@"Unable to interpret bytes as a valid image." - userInfo:nil]; - } - } else { - NSString *error = [NSString - stringWithFormat:@"fromBytes should have exactly one argument, the bytes. Got: %lu", - (unsigned long)iconData.count]; - NSException *exception = [NSException exceptionWithName:@"InvalidByteDescriptor" - reason:error - userInfo:nil]; - @throw exception; - } - } - - return image; -} - -@implementation FLTMarkersController { - NSMutableDictionary *_markerIdToController; - FlutterMethodChannel *_methodChannel; - NSObject *_registrar; - GMSMapView *_mapView; -} -- (instancetype)init:(FlutterMethodChannel *)methodChannel - mapView:(GMSMapView *)mapView - registrar:(NSObject *)registrar { - self = [super init]; - if (self) { - _methodChannel = methodChannel; - _mapView = mapView; - _markerIdToController = [NSMutableDictionary dictionaryWithCapacity:1]; - _registrar = registrar; - } - return self; -} -- (void)addMarkers:(NSArray *)markersToAdd { - for (NSDictionary *marker in markersToAdd) { - CLLocationCoordinate2D position = [FLTMarkersController getPosition:marker]; - NSString *markerId = [FLTMarkersController getMarkerId:marker]; - FLTGoogleMapMarkerController *controller = - [[FLTGoogleMapMarkerController alloc] initMarkerWithPosition:position - markerId:markerId - mapView:_mapView]; - InterpretMarkerOptions(marker, controller, _registrar); - _markerIdToController[markerId] = controller; - } -} -- (void)changeMarkers:(NSArray *)markersToChange { - for (NSDictionary *marker in markersToChange) { - NSString *markerId = [FLTMarkersController getMarkerId:marker]; - FLTGoogleMapMarkerController *controller = _markerIdToController[markerId]; - if (!controller) { - continue; - } - InterpretMarkerOptions(marker, controller, _registrar); - } -} -- (void)removeMarkerIds:(NSArray *)markerIdsToRemove { - for (NSString *markerId in markerIdsToRemove) { - if (!markerId) { - continue; - } - FLTGoogleMapMarkerController *controller = _markerIdToController[markerId]; - if (!controller) { - continue; - } - [controller removeMarker]; - [_markerIdToController removeObjectForKey:markerId]; - } -} -- (BOOL)onMarkerTap:(NSString *)markerId { - if (!markerId) { - return NO; - } - FLTGoogleMapMarkerController *controller = _markerIdToController[markerId]; - if (!controller) { - return NO; - } - [_methodChannel invokeMethod:@"marker#onTap" arguments:@{@"markerId" : markerId}]; - return controller.consumeTapEvents; -} -- (void)onMarkerDragStart:(NSString *)markerId coordinate:(CLLocationCoordinate2D)coordinate { - if (!markerId) { - return; - } - FLTGoogleMapMarkerController *controller = _markerIdToController[markerId]; - if (!controller) { - return; - } - [_methodChannel invokeMethod:@"marker#onDragStart" - arguments:@{@"markerId" : markerId, @"position" : PositionToJson(coordinate)}]; -} -- (void)onMarkerDrag:(NSString *)markerId coordinate:(CLLocationCoordinate2D)coordinate { - if (!markerId) { - return; - } - FLTGoogleMapMarkerController *controller = _markerIdToController[markerId]; - if (!controller) { - return; - } - [_methodChannel invokeMethod:@"marker#onDrag" - arguments:@{@"markerId" : markerId, @"position" : PositionToJson(coordinate)}]; -} -- (void)onMarkerDragEnd:(NSString *)markerId coordinate:(CLLocationCoordinate2D)coordinate { - if (!markerId) { - return; - } - FLTGoogleMapMarkerController *controller = _markerIdToController[markerId]; - if (!controller) { - return; - } - [_methodChannel invokeMethod:@"marker#onDragEnd" - arguments:@{@"markerId" : markerId, @"position" : PositionToJson(coordinate)}]; -} -- (void)onInfoWindowTap:(NSString *)markerId { - if (markerId && _markerIdToController[markerId]) { - [_methodChannel invokeMethod:@"infoWindow#onTap" arguments:@{@"markerId" : markerId}]; - } -} -- (void)showMarkerInfoWindow:(NSString *)markerId result:(FlutterResult)result { - FLTGoogleMapMarkerController *controller = _markerIdToController[markerId]; - if (controller) { - [controller showInfoWindow]; - result(nil); - } else { - result([FlutterError errorWithCode:@"Invalid markerId" - message:@"showInfoWindow called with invalid markerId" - details:nil]); - } -} -- (void)hideMarkerInfoWindow:(NSString *)markerId result:(FlutterResult)result { - FLTGoogleMapMarkerController *controller = _markerIdToController[markerId]; - if (controller) { - [controller hideInfoWindow]; - result(nil); - } else { - result([FlutterError errorWithCode:@"Invalid markerId" - message:@"hideInfoWindow called with invalid markerId" - details:nil]); - } -} -- (void)isMarkerInfoWindowShown:(NSString *)markerId result:(FlutterResult)result { - FLTGoogleMapMarkerController *controller = _markerIdToController[markerId]; - if (controller) { - result(@([controller isInfoWindowShown])); - } else { - result([FlutterError errorWithCode:@"Invalid markerId" - message:@"isInfoWindowShown called with invalid markerId" - details:nil]); - } -} - -+ (CLLocationCoordinate2D)getPosition:(NSDictionary *)marker { - NSArray *position = marker[@"position"]; - return ToLocation(position); -} -+ (NSString *)getMarkerId:(NSDictionary *)marker { - return marker[@"markerId"]; -} -@end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h deleted file mode 100644 index bdc5dd4bf850..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h +++ /dev/null @@ -1,38 +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 -#import - -// Defines polygon UI options writable from Flutter. -@protocol FLTGoogleMapPolygonOptionsSink -- (void)setConsumeTapEvents:(BOOL)consume; -- (void)setVisible:(BOOL)visible; -- (void)setFillColor:(UIColor *)color; -- (void)setStrokeColor:(UIColor *)color; -- (void)setStrokeWidth:(CGFloat)width; -- (void)setPoints:(NSArray *)points; -- (void)setHoles:(NSArray *> *)holes; -- (void)setZIndex:(int)zIndex; -@end - -// Defines polygon controllable by Flutter. -@interface FLTGoogleMapPolygonController : NSObject -@property(atomic, readonly) NSString *polygonId; -- (instancetype)initPolygonWithPath:(GMSMutablePath *)path - polygonId:(NSString *)polygonId - mapView:(GMSMapView *)mapView; -- (void)removePolygon; -@end - -@interface FLTPolygonsController : NSObject -- (instancetype)init:(FlutterMethodChannel *)methodChannel - mapView:(GMSMapView *)mapView - registrar:(NSObject *)registrar; -- (void)addPolygons:(NSArray *)polygonsToAdd; -- (void)changePolygons:(NSArray *)polygonsToChange; -- (void)removePolygonIds:(NSArray *)polygonIdsToRemove; -- (void)onPolygonTap:(NSString *)polygonId; -- (bool)hasPolygonWithId:(NSString *)polygonId; -@end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m deleted file mode 100644 index 649ba98bca13..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m +++ /dev/null @@ -1,211 +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 "GoogleMapPolygonController.h" -#import "JsonConversions.h" - -@implementation FLTGoogleMapPolygonController { - GMSPolygon *_polygon; - GMSMapView *_mapView; -} -- (instancetype)initPolygonWithPath:(GMSMutablePath *)path - polygonId:(NSString *)polygonId - mapView:(GMSMapView *)mapView { - self = [super init]; - if (self) { - _polygon = [GMSPolygon polygonWithPath:path]; - _mapView = mapView; - _polygonId = polygonId; - _polygon.userData = @[ polygonId ]; - } - return self; -} - -- (void)removePolygon { - _polygon.map = nil; -} - -#pragma mark - FLTGoogleMapPolygonOptionsSink methods - -- (void)setConsumeTapEvents:(BOOL)consumes { - _polygon.tappable = consumes; -} -- (void)setVisible:(BOOL)visible { - _polygon.map = visible ? _mapView : nil; -} -- (void)setZIndex:(int)zIndex { - _polygon.zIndex = zIndex; -} -- (void)setPoints:(NSArray *)points { - GMSMutablePath *path = [GMSMutablePath path]; - - for (CLLocation *location in points) { - [path addCoordinate:location.coordinate]; - } - _polygon.path = path; -} -- (void)setHoles:(NSArray *> *)rawHoles { - NSMutableArray *holes = [[NSMutableArray alloc] init]; - - for (NSArray *points in rawHoles) { - GMSMutablePath *path = [GMSMutablePath path]; - for (CLLocation *location in points) { - [path addCoordinate:location.coordinate]; - } - [holes addObject:path]; - } - - _polygon.holes = holes; -} - -- (void)setFillColor:(UIColor *)color { - _polygon.fillColor = color; -} -- (void)setStrokeColor:(UIColor *)color { - _polygon.strokeColor = color; -} -- (void)setStrokeWidth:(CGFloat)width { - _polygon.strokeWidth = width; -} -@end - -static int ToInt(NSNumber *data) { return [FLTGoogleMapJsonConversions toInt:data]; } - -static BOOL ToBool(NSNumber *data) { return [FLTGoogleMapJsonConversions toBool:data]; } - -static NSArray *ToPoints(NSArray *data) { - return [FLTGoogleMapJsonConversions toPoints:data]; -} - -static NSArray *> *ToHoles(NSArray *data) { - return [FLTGoogleMapJsonConversions toHoles:data]; -} - -static UIColor *ToColor(NSNumber *data) { return [FLTGoogleMapJsonConversions toColor:data]; } - -static void InterpretPolygonOptions(NSDictionary *data, id sink, - NSObject *registrar) { - NSNumber *consumeTapEvents = data[@"consumeTapEvents"]; - if (consumeTapEvents != nil) { - [sink setConsumeTapEvents:ToBool(consumeTapEvents)]; - } - - NSNumber *visible = data[@"visible"]; - if (visible != nil) { - [sink setVisible:ToBool(visible)]; - } - - NSNumber *zIndex = data[@"zIndex"]; - if (zIndex != nil) { - [sink setZIndex:ToInt(zIndex)]; - } - - NSArray *points = data[@"points"]; - if (points) { - [sink setPoints:ToPoints(points)]; - } - - NSArray *holes = data[@"holes"]; - if (holes) { - [sink setHoles:ToHoles(holes)]; - } - - NSNumber *fillColor = data[@"fillColor"]; - if (fillColor != nil) { - [sink setFillColor:ToColor(fillColor)]; - } - - NSNumber *strokeColor = data[@"strokeColor"]; - if (strokeColor != nil) { - [sink setStrokeColor:ToColor(strokeColor)]; - } - - NSNumber *strokeWidth = data[@"strokeWidth"]; - if (strokeWidth != nil) { - [sink setStrokeWidth:ToInt(strokeWidth)]; - } -} - -@implementation FLTPolygonsController { - NSMutableDictionary *_polygonIdToController; - FlutterMethodChannel *_methodChannel; - NSObject *_registrar; - GMSMapView *_mapView; -} -- (instancetype)init:(FlutterMethodChannel *)methodChannel - mapView:(GMSMapView *)mapView - registrar:(NSObject *)registrar { - self = [super init]; - if (self) { - _methodChannel = methodChannel; - _mapView = mapView; - _polygonIdToController = [NSMutableDictionary dictionaryWithCapacity:1]; - _registrar = registrar; - } - return self; -} -- (void)addPolygons:(NSArray *)polygonsToAdd { - for (NSDictionary *polygon in polygonsToAdd) { - GMSMutablePath *path = [FLTPolygonsController getPath:polygon]; - NSString *polygonId = [FLTPolygonsController getPolygonId:polygon]; - FLTGoogleMapPolygonController *controller = - [[FLTGoogleMapPolygonController alloc] initPolygonWithPath:path - polygonId:polygonId - mapView:_mapView]; - InterpretPolygonOptions(polygon, controller, _registrar); - _polygonIdToController[polygonId] = controller; - } -} -- (void)changePolygons:(NSArray *)polygonsToChange { - for (NSDictionary *polygon in polygonsToChange) { - NSString *polygonId = [FLTPolygonsController getPolygonId:polygon]; - FLTGoogleMapPolygonController *controller = _polygonIdToController[polygonId]; - if (!controller) { - continue; - } - InterpretPolygonOptions(polygon, controller, _registrar); - } -} -- (void)removePolygonIds:(NSArray *)polygonIdsToRemove { - for (NSString *polygonId in polygonIdsToRemove) { - if (!polygonId) { - continue; - } - FLTGoogleMapPolygonController *controller = _polygonIdToController[polygonId]; - if (!controller) { - continue; - } - [controller removePolygon]; - [_polygonIdToController removeObjectForKey:polygonId]; - } -} -- (void)onPolygonTap:(NSString *)polygonId { - if (!polygonId) { - return; - } - FLTGoogleMapPolygonController *controller = _polygonIdToController[polygonId]; - if (!controller) { - return; - } - [_methodChannel invokeMethod:@"polygon#onTap" arguments:@{@"polygonId" : polygonId}]; -} -- (bool)hasPolygonWithId:(NSString *)polygonId { - if (!polygonId) { - return false; - } - return _polygonIdToController[polygonId] != nil; -} -+ (GMSMutablePath *)getPath:(NSDictionary *)polygon { - NSArray *pointArray = polygon[@"points"]; - NSArray *points = ToPoints(pointArray); - GMSMutablePath *path = [GMSMutablePath path]; - for (CLLocation *location in points) { - [path addCoordinate:location.coordinate]; - } - return path; -} -+ (NSString *)getPolygonId:(NSDictionary *)polygon { - return polygon[@"polygonId"]; -} -@end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.m deleted file mode 100644 index f366051b4af2..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.m +++ /dev/null @@ -1,190 +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 "GoogleMapPolylineController.h" -#import "JsonConversions.h" - -@implementation FLTGoogleMapPolylineController { - GMSPolyline *_polyline; - GMSMapView *_mapView; -} -- (instancetype)initPolylineWithPath:(GMSMutablePath *)path - polylineId:(NSString *)polylineId - mapView:(GMSMapView *)mapView { - self = [super init]; - if (self) { - _polyline = [GMSPolyline polylineWithPath:path]; - _mapView = mapView; - _polylineId = polylineId; - _polyline.userData = @[ polylineId ]; - } - return self; -} - -- (void)removePolyline { - _polyline.map = nil; -} - -#pragma mark - FLTGoogleMapPolylineOptionsSink methods - -- (void)setConsumeTapEvents:(BOOL)consumes { - _polyline.tappable = consumes; -} -- (void)setVisible:(BOOL)visible { - _polyline.map = visible ? _mapView : nil; -} -- (void)setZIndex:(int)zIndex { - _polyline.zIndex = zIndex; -} -- (void)setPoints:(NSArray *)points { - GMSMutablePath *path = [GMSMutablePath path]; - - for (CLLocation *location in points) { - [path addCoordinate:location.coordinate]; - } - _polyline.path = path; -} - -- (void)setColor:(UIColor *)color { - _polyline.strokeColor = color; -} -- (void)setStrokeWidth:(CGFloat)width { - _polyline.strokeWidth = width; -} - -- (void)setGeodesic:(BOOL)isGeodesic { - _polyline.geodesic = isGeodesic; -} -@end - -static int ToInt(NSNumber *data) { return [FLTGoogleMapJsonConversions toInt:data]; } - -static BOOL ToBool(NSNumber *data) { return [FLTGoogleMapJsonConversions toBool:data]; } - -static NSArray *ToPoints(NSArray *data) { - return [FLTGoogleMapJsonConversions toPoints:data]; -} - -static UIColor *ToColor(NSNumber *data) { return [FLTGoogleMapJsonConversions toColor:data]; } - -static void InterpretPolylineOptions(NSDictionary *data, id sink, - NSObject *registrar) { - NSNumber *consumeTapEvents = data[@"consumeTapEvents"]; - if (consumeTapEvents != nil) { - [sink setConsumeTapEvents:ToBool(consumeTapEvents)]; - } - - NSNumber *visible = data[@"visible"]; - if (visible != nil) { - [sink setVisible:ToBool(visible)]; - } - - NSNumber *zIndex = data[@"zIndex"]; - if (zIndex != nil) { - [sink setZIndex:ToInt(zIndex)]; - } - - NSArray *points = data[@"points"]; - if (points) { - [sink setPoints:ToPoints(points)]; - } - - NSNumber *strokeColor = data[@"color"]; - if (strokeColor != nil) { - [sink setColor:ToColor(strokeColor)]; - } - - NSNumber *strokeWidth = data[@"width"]; - if (strokeWidth != nil) { - [sink setStrokeWidth:ToInt(strokeWidth)]; - } - - NSNumber *geodesic = data[@"geodesic"]; - if (geodesic != nil) { - [sink setGeodesic:geodesic.boolValue]; - } -} - -@implementation FLTPolylinesController { - NSMutableDictionary *_polylineIdToController; - FlutterMethodChannel *_methodChannel; - NSObject *_registrar; - GMSMapView *_mapView; -} -- (instancetype)init:(FlutterMethodChannel *)methodChannel - mapView:(GMSMapView *)mapView - registrar:(NSObject *)registrar { - self = [super init]; - if (self) { - _methodChannel = methodChannel; - _mapView = mapView; - _polylineIdToController = [NSMutableDictionary dictionaryWithCapacity:1]; - _registrar = registrar; - } - return self; -} -- (void)addPolylines:(NSArray *)polylinesToAdd { - for (NSDictionary *polyline in polylinesToAdd) { - GMSMutablePath *path = [FLTPolylinesController getPath:polyline]; - NSString *polylineId = [FLTPolylinesController getPolylineId:polyline]; - FLTGoogleMapPolylineController *controller = - [[FLTGoogleMapPolylineController alloc] initPolylineWithPath:path - polylineId:polylineId - mapView:_mapView]; - InterpretPolylineOptions(polyline, controller, _registrar); - _polylineIdToController[polylineId] = controller; - } -} -- (void)changePolylines:(NSArray *)polylinesToChange { - for (NSDictionary *polyline in polylinesToChange) { - NSString *polylineId = [FLTPolylinesController getPolylineId:polyline]; - FLTGoogleMapPolylineController *controller = _polylineIdToController[polylineId]; - if (!controller) { - continue; - } - InterpretPolylineOptions(polyline, controller, _registrar); - } -} -- (void)removePolylineIds:(NSArray *)polylineIdsToRemove { - for (NSString *polylineId in polylineIdsToRemove) { - if (!polylineId) { - continue; - } - FLTGoogleMapPolylineController *controller = _polylineIdToController[polylineId]; - if (!controller) { - continue; - } - [controller removePolyline]; - [_polylineIdToController removeObjectForKey:polylineId]; - } -} -- (void)onPolylineTap:(NSString *)polylineId { - if (!polylineId) { - return; - } - FLTGoogleMapPolylineController *controller = _polylineIdToController[polylineId]; - if (!controller) { - return; - } - [_methodChannel invokeMethod:@"polyline#onTap" arguments:@{@"polylineId" : polylineId}]; -} -- (bool)hasPolylineWithId:(NSString *)polylineId { - if (!polylineId) { - return false; - } - return _polylineIdToController[polylineId] != nil; -} -+ (GMSMutablePath *)getPath:(NSDictionary *)polyline { - NSArray *pointArray = polyline[@"points"]; - NSArray *points = ToPoints(pointArray); - GMSMutablePath *path = [GMSMutablePath path]; - for (CLLocation *location in points) { - [path addCoordinate:location.coordinate]; - } - return path; -} -+ (NSString *)getPolylineId:(NSDictionary *)polyline { - return polyline[@"polylineId"]; -} -@end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.h deleted file mode 100644 index c0f673ecd025..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.h +++ /dev/null @@ -1,19 +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 -#import - -@interface FLTGoogleMapJsonConversions : NSObject -+ (bool)toBool:(NSNumber *)data; -+ (int)toInt:(NSNumber *)data; -+ (double)toDouble:(NSNumber *)data; -+ (float)toFloat:(NSNumber *)data; -+ (CLLocationCoordinate2D)toLocation:(NSArray *)data; -+ (CGPoint)toPoint:(NSArray *)data; -+ (NSArray *)positionToJson:(CLLocationCoordinate2D)position; -+ (UIColor *)toColor:(NSNumber *)data; -+ (NSArray *)toPoints:(NSArray *)data; -+ (NSArray *> *)toHoles:(NSArray *)data; -@end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.m deleted file mode 100644 index 0e88d4707489..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.m +++ /dev/null @@ -1,71 +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 "JsonConversions.h" - -@implementation FLTGoogleMapJsonConversions - -+ (bool)toBool:(NSNumber *)data { - return data.boolValue; -} - -+ (int)toInt:(NSNumber *)data { - return data.intValue; -} - -+ (double)toDouble:(NSNumber *)data { - return data.doubleValue; -} - -+ (float)toFloat:(NSNumber *)data { - return data.floatValue; -} - -+ (CLLocationCoordinate2D)toLocation:(NSArray *)data { - return CLLocationCoordinate2DMake([FLTGoogleMapJsonConversions toDouble:data[0]], - [FLTGoogleMapJsonConversions toDouble:data[1]]); -} - -+ (CGPoint)toPoint:(NSArray *)data { - return CGPointMake([FLTGoogleMapJsonConversions toDouble:data[0]], - [FLTGoogleMapJsonConversions toDouble:data[1]]); -} - -+ (NSArray *)positionToJson:(CLLocationCoordinate2D)position { - return @[ @(position.latitude), @(position.longitude) ]; -} - -+ (UIColor *)toColor:(NSNumber *)numberColor { - unsigned long value = [numberColor unsignedLongValue]; - return [UIColor colorWithRed:((float)((value & 0xFF0000) >> 16)) / 255.0 - green:((float)((value & 0xFF00) >> 8)) / 255.0 - blue:((float)(value & 0xFF)) / 255.0 - alpha:((float)((value & 0xFF000000) >> 24)) / 255.0]; -} - -+ (NSArray *)toPoints:(NSArray *)data { - NSMutableArray *points = [[NSMutableArray alloc] init]; - for (unsigned i = 0; i < [data count]; i++) { - NSNumber *latitude = data[i][0]; - NSNumber *longitude = data[i][1]; - CLLocation *point = - [[CLLocation alloc] initWithLatitude:[FLTGoogleMapJsonConversions toDouble:latitude] - longitude:[FLTGoogleMapJsonConversions toDouble:longitude]]; - [points addObject:point]; - } - - return points; -} - -+ (NSArray *> *)toHoles:(NSArray *)data { - NSMutableArray *> *holes = [[[NSMutableArray alloc] init] init]; - for (unsigned i = 0; i < [data count]; i++) { - NSArray *points = [FLTGoogleMapJsonConversions toPoints:data[i]]; - [holes addObject:points]; - } - - return holes; -} - -@end diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart index 5b1e67c943dd..a4be120b2117 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart @@ -6,13 +6,14 @@ library google_maps_flutter; import 'dart:async'; import 'dart:io'; +// 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 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index 088589e4a2ce..cd3d0781e471 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: library_private_types_in_public_api + part of google_maps_flutter; /// Controller for a single GoogleMap instance running on the host platform. @@ -33,20 +35,6 @@ class GoogleMapController { ); } - /// Used to communicate with the native platform. - /// - /// Accessible only for testing. - // TODO(dit): Remove this getter, https://github.com/flutter/flutter/issues/55504. - @visibleForTesting - MethodChannel? get channel { - if (GoogleMapsFlutterPlatform.instance is MethodChannelGoogleMapsFlutter) { - return (GoogleMapsFlutterPlatform.instance - as MethodChannelGoogleMapsFlutter) - .channel(mapId); - } - return null; - } - final _GoogleMapState _googleMapState; void _connectStreams(int mapId) { @@ -100,10 +88,9 @@ class GoogleMapController { /// platform side. /// /// The returned [Future] completes after listeners have been notified. - Future _updateMapOptions(Map optionsUpdate) { - assert(optionsUpdate != null); + Future _updateMapConfiguration(MapConfiguration update) { return GoogleMapsFlutterPlatform.instance - .updateMapOptions(optionsUpdate, mapId: mapId); + .updateMapConfiguration(update, mapId: mapId); } /// Updates marker configuration. diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index 8ecbfbbad499..1f7871068cab 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart @@ -40,7 +40,11 @@ class UnknownMapObjectIdError extends Error { } /// Android specific settings for [GoogleMap]. +@Deprecated( + 'See https://pub.dev/packages/google_maps_flutter_android#display-mode') class AndroidGoogleMapsFlutter { + @Deprecated( + 'See https://pub.dev/packages/google_maps_flutter_android#display-mode') AndroidGoogleMapsFlutter._(); /// Whether to render [GoogleMap] with a [AndroidViewSurface] to build the Google Maps widget. @@ -50,12 +54,12 @@ class AndroidGoogleMapsFlutter { /// versions below 10. See /// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more /// information. - /// - /// Defaults to false. + @Deprecated( + 'See https://pub.dev/packages/google_maps_flutter_android#display-mode') static bool get useAndroidViewSurface { final GoogleMapsFlutterPlatform platform = GoogleMapsFlutterPlatform.instance; - if (platform is MethodChannelGoogleMapsFlutter) { + if (platform is GoogleMapsFlutterAndroid) { return platform.useAndroidViewSurface; } return false; @@ -68,12 +72,12 @@ class AndroidGoogleMapsFlutter { /// versions below 10. See /// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more /// information. - /// - /// Defaults to false. + @Deprecated( + 'See https://pub.dev/packages/google_maps_flutter_android#display-mode') static set useAndroidViewSurface(bool useAndroidViewSurface) { final GoogleMapsFlutterPlatform platform = GoogleMapsFlutterPlatform.instance; - if (platform is MethodChannelGoogleMapsFlutter) { + if (platform is GoogleMapsFlutterAndroid) { platform.useAndroidViewSurface = useAndroidViewSurface; } } @@ -105,7 +109,7 @@ class GoogleMap extends StatefulWidget { this.layoutDirection, /// If no padding is specified default padding will be 0. - this.padding = const EdgeInsets.all(0), + this.padding = EdgeInsets.zero, this.indoorViewEnabled = false, this.trafficEnabled = false, this.buildingsEnabled = true, @@ -294,30 +298,34 @@ class _GoogleMapState extends State { Map _polygons = {}; Map _polylines = {}; Map _circles = {}; - late _GoogleMapOptions _googleMapOptions; + late MapConfiguration _mapConfiguration; @override Widget build(BuildContext context) { - return GoogleMapsFlutterPlatform.instance.buildViewWithTextDirection( + return GoogleMapsFlutterPlatform.instance.buildViewWithConfiguration( _mapId, onPlatformViewCreated, - textDirection: widget.layoutDirection ?? - Directionality.maybeOf(context) ?? - TextDirection.ltr, - initialCameraPosition: widget.initialCameraPosition, - markers: widget.markers, - polygons: widget.polygons, - polylines: widget.polylines, - circles: widget.circles, - gestureRecognizers: widget.gestureRecognizers, - mapOptions: _googleMapOptions.toMap(), + widgetConfiguration: MapWidgetConfiguration( + textDirection: widget.layoutDirection ?? + Directionality.maybeOf(context) ?? + TextDirection.ltr, + initialCameraPosition: widget.initialCameraPosition, + gestureRecognizers: widget.gestureRecognizers, + ), + mapObjects: MapObjects( + markers: widget.markers, + polygons: widget.polygons, + polylines: widget.polylines, + circles: widget.circles, + ), + mapConfiguration: _mapConfiguration, ); } @override void initState() { super.initState(); - _googleMapOptions = _GoogleMapOptions.fromWidget(widget); + _mapConfiguration = _configurationFromMapWidget(widget); _markers = keyByMarkerId(widget.markers); _polygons = keyByPolygonId(widget.polygons); _polylines = keyByPolylineId(widget.polylines); @@ -347,16 +355,15 @@ class _GoogleMapState extends State { } Future _updateOptions() async { - final _GoogleMapOptions newOptions = _GoogleMapOptions.fromWidget(widget); - final Map updates = - _googleMapOptions.updatesMap(newOptions); + final MapConfiguration newConfig = _configurationFromMapWidget(widget); + final MapConfiguration updates = newConfig.diffFrom(_mapConfiguration); if (updates.isEmpty) { return; } final GoogleMapController controller = await _controller.future; // ignore: unawaited_futures - controller._updateMapOptions(updates); - _googleMapOptions = newOptions; + controller._updateMapConfiguration(updates); + _mapConfiguration = newConfig; } Future _updateMarkers() async { @@ -524,98 +531,27 @@ class _GoogleMapState extends State { } } -/// Configuration options for the GoogleMaps user interface. -class _GoogleMapOptions { - _GoogleMapOptions.fromWidget(GoogleMap map) - : compassEnabled = map.compassEnabled, - mapToolbarEnabled = map.mapToolbarEnabled, - cameraTargetBounds = map.cameraTargetBounds, - mapType = map.mapType, - minMaxZoomPreference = map.minMaxZoomPreference, - rotateGesturesEnabled = map.rotateGesturesEnabled, - scrollGesturesEnabled = map.scrollGesturesEnabled, - tiltGesturesEnabled = map.tiltGesturesEnabled, - trackCameraPosition = map.onCameraMove != null, - zoomControlsEnabled = map.zoomControlsEnabled, - zoomGesturesEnabled = map.zoomGesturesEnabled, - liteModeEnabled = map.liteModeEnabled, - myLocationEnabled = map.myLocationEnabled, - myLocationButtonEnabled = map.myLocationButtonEnabled, - padding = map.padding, - indoorViewEnabled = map.indoorViewEnabled, - trafficEnabled = map.trafficEnabled, - buildingsEnabled = map.buildingsEnabled, - assert(!map.liteModeEnabled || Platform.isAndroid); - - final bool compassEnabled; - - final bool mapToolbarEnabled; - - final CameraTargetBounds cameraTargetBounds; - - final MapType mapType; - - final MinMaxZoomPreference minMaxZoomPreference; - - final bool rotateGesturesEnabled; - - final bool scrollGesturesEnabled; - - final bool tiltGesturesEnabled; - - final bool trackCameraPosition; - - final bool zoomControlsEnabled; - - final bool zoomGesturesEnabled; - - final bool liteModeEnabled; - - final bool myLocationEnabled; - - final bool myLocationButtonEnabled; - - final EdgeInsets padding; - - final bool indoorViewEnabled; - - final bool trafficEnabled; - - final bool buildingsEnabled; - - Map toMap() { - return { - 'compassEnabled': compassEnabled, - 'mapToolbarEnabled': mapToolbarEnabled, - 'cameraTargetBounds': cameraTargetBounds.toJson(), - 'mapType': mapType.index, - 'minMaxZoomPreference': minMaxZoomPreference.toJson(), - 'rotateGesturesEnabled': rotateGesturesEnabled, - 'scrollGesturesEnabled': scrollGesturesEnabled, - 'tiltGesturesEnabled': tiltGesturesEnabled, - 'zoomControlsEnabled': zoomControlsEnabled, - 'zoomGesturesEnabled': zoomGesturesEnabled, - 'liteModeEnabled': liteModeEnabled, - 'trackCameraPosition': trackCameraPosition, - 'myLocationEnabled': myLocationEnabled, - 'myLocationButtonEnabled': myLocationButtonEnabled, - 'padding': [ - padding.top, - padding.left, - padding.bottom, - padding.right, - ], - 'indoorEnabled': indoorViewEnabled, - 'trafficEnabled': trafficEnabled, - 'buildingsEnabled': buildingsEnabled, - }; - } - - Map updatesMap(_GoogleMapOptions newOptions) { - final Map prevOptionsMap = toMap(); - - return newOptions.toMap() - ..removeWhere( - (String key, dynamic value) => prevOptionsMap[key] == value); - } +/// Builds a [MapConfiguration] from the given [map]. +MapConfiguration _configurationFromMapWidget(GoogleMap map) { + assert(!map.liteModeEnabled || Platform.isAndroid); + return MapConfiguration( + compassEnabled: map.compassEnabled, + mapToolbarEnabled: map.mapToolbarEnabled, + cameraTargetBounds: map.cameraTargetBounds, + mapType: map.mapType, + minMaxZoomPreference: map.minMaxZoomPreference, + rotateGesturesEnabled: map.rotateGesturesEnabled, + scrollGesturesEnabled: map.scrollGesturesEnabled, + tiltGesturesEnabled: map.tiltGesturesEnabled, + trackCameraPosition: map.onCameraMove != null, + zoomControlsEnabled: map.zoomControlsEnabled, + zoomGesturesEnabled: map.zoomGesturesEnabled, + liteModeEnabled: map.liteModeEnabled, + myLocationEnabled: map.myLocationEnabled, + myLocationButtonEnabled: map.myLocationButtonEnabled, + padding: map.padding, + indoorViewEnabled: map.indoorViewEnabled, + trafficEnabled: map.trafficEnabled, + buildingsEnabled: map.buildingsEnabled, + ); } diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 849019cbdb2d..540f5d810966 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -2,31 +2,29 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.1.2 +version: 2.2.1 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: platforms: android: - package: io.flutter.plugins.googlemaps - pluginClass: GoogleMapsPlugin + default_package: google_maps_flutter_android ios: - pluginClass: FLTGoogleMapsPlugin + default_package: google_maps_flutter_ios dependencies: flutter: sdk: flutter - flutter_plugin_android_lifecycle: ^2.0.1 - google_maps_flutter_platform_interface: ^2.1.2 + google_maps_flutter_android: ^2.1.10 + google_maps_flutter_ios: ^2.1.10 + google_maps_flutter_platform_interface: ^2.2.1 dev_dependencies: flutter_test: sdk: flutter - - pedantic: ^1.10.0 plugin_platform_interface: ^2.0.0 stream_transform: ^2.0.0 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 bac3ceabc4de..9fe6923204ab 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 @@ -12,8 +12,7 @@ class FakePlatformGoogleMap { FakePlatformGoogleMap(int id, Map params) : cameraPosition = CameraPosition.fromMap(params['initialCameraPosition']), - channel = MethodChannel( - 'plugins.flutter.io/google_maps_$id', const StandardMethodCodec()) { + channel = MethodChannel('plugins.flutter.io/google_maps_$id') { channel.setMockMethodCallHandler(onMethodCall); updateOptions(params['options'] as Map); updateMarkers(params); 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 003ae06d9877..0fc5e7723df5 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 @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'fake_maps_controllers.dart'; @@ -90,7 +89,6 @@ void main() { textDirection: TextDirection.ltr, child: GoogleMap( initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - compassEnabled: true, ), ), ); @@ -119,7 +117,6 @@ void main() { textDirection: TextDirection.ltr, child: GoogleMap( initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - mapToolbarEnabled: true, ), ), ); @@ -233,7 +230,6 @@ void main() { textDirection: TextDirection.ltr, child: GoogleMap( initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - minMaxZoomPreference: MinMaxZoomPreference.unbounded, ), ), ); @@ -263,7 +259,6 @@ void main() { textDirection: TextDirection.ltr, child: GoogleMap( initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - rotateGesturesEnabled: true, ), ), ); @@ -292,7 +287,6 @@ void main() { textDirection: TextDirection.ltr, child: GoogleMap( initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - scrollGesturesEnabled: true, ), ), ); @@ -321,7 +315,6 @@ void main() { textDirection: TextDirection.ltr, child: GoogleMap( initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - tiltGesturesEnabled: true, ), ), ); @@ -379,7 +372,6 @@ void main() { textDirection: TextDirection.ltr, child: GoogleMap( initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - zoomGesturesEnabled: true, ), ), ); @@ -408,7 +400,6 @@ void main() { textDirection: TextDirection.ltr, child: GoogleMap( initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - zoomControlsEnabled: true, ), ), ); @@ -422,7 +413,6 @@ void main() { textDirection: TextDirection.ltr, child: GoogleMap( initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - myLocationEnabled: false, ), ), ); @@ -452,7 +442,6 @@ void main() { textDirection: TextDirection.ltr, child: GoogleMap( initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - myLocationEnabled: false, ), ), ); @@ -537,7 +526,6 @@ void main() { textDirection: TextDirection.ltr, child: GoogleMap( initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - trafficEnabled: false, ), ), ); @@ -581,45 +569,10 @@ void main() { textDirection: TextDirection.ltr, child: GoogleMap( initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - buildingsEnabled: true, ), ), ); expect(platformGoogleMap.buildingsEnabled, true); }); - - testWidgets( - 'Default Android widget is AndroidView', - (WidgetTester tester) async { - await tester.pumpWidget( - const Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - ), - ), - ); - - expect(find.byType(AndroidView), findsOneWidget); - }, - ); - - testWidgets('Use PlatformViewLink on Android', (WidgetTester tester) async { - final MethodChannelGoogleMapsFlutter platform = - GoogleMapsFlutterPlatform.instance as MethodChannelGoogleMapsFlutter; - platform.useAndroidViewSurface = true; - - await tester.pumpWidget( - const Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - ), - ), - ); - - expect(find.byType(PlatformViewLink), findsOneWidget); - platform.useAndroidViewSurface = false; - }); } diff --git a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart index 73e1e77646cd..b34fccbfa422 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 @@ -3,10 +3,10 @@ // found in the LICENSE file. 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'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -88,8 +88,8 @@ class TestGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { Future init(int mapId) async {} @override - Future updateMapOptions( - Map optionsUpdate, { + Future updateMapConfiguration( + MapConfiguration update, { required int mapId, }) async {} @@ -276,18 +276,12 @@ class TestGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { } @override - Widget buildView( + Widget buildViewWithConfiguration( int creationId, PlatformViewCreatedCallback onPlatformViewCreated, { - required CameraPosition initialCameraPosition, - Set markers = const {}, - Set polygons = const {}, - Set polylines = const {}, - Set circles = const {}, - Set tileOverlays = const {}, - Set>? gestureRecognizers = - const >{}, - Map mapOptions = const {}, + required MapWidgetConfiguration widgetConfiguration, + MapObjects mapObjects = const MapObjects(), + MapConfiguration mapConfiguration = const MapConfiguration(), }) { onPlatformViewCreated(0); createdIds.add(creationId); 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 cb7263c02e05..34959832b36b 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 @@ -123,10 +123,10 @@ void main() { }); testWidgets('Mutate a polygon', (WidgetTester tester) async { - final List _points = [const LatLng(0.0, 0.0)]; + final List points = [const LatLng(0.0, 0.0)]; final Polygon p1 = Polygon( polygonId: const PolygonId('polygon_1'), - points: _points, + points: points, ); await tester.pumpWidget(_mapWithPolygons({p1})); 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 9cbba3a510ca..f9d091695383 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 @@ -117,10 +117,10 @@ void main() { }); testWidgets('Mutate a polyline', (WidgetTester tester) async { - final List _points = [const LatLng(0.0, 0.0)]; + final List points = [const LatLng(0.0, 0.0)]; final Polyline p1 = Polyline( polylineId: const PolylineId('polyline_1'), - points: _points, + points: points, ); await tester.pumpWidget(_mapWithPolylines({p1})); @@ -200,8 +200,7 @@ void main() { }); testWidgets('Update non platform related attr', (WidgetTester tester) async { - Polyline p1 = - const Polyline(polylineId: PolylineId('polyline_1'), onTap: null); + Polyline p1 = const Polyline(polylineId: PolylineId('polyline_1')); final Set prev = {p1}; p1 = Polyline( polylineId: const PolylineId('polyline_1'), onTap: () => print(2 + 2)); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/AUTHORS b/packages/google_maps_flutter/google_maps_flutter_android/AUTHORS new file mode 100644 index 000000000000..9f1b53ee2667 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Taha Tesser diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md new file mode 100644 index 000000000000..9bc8b195f6e2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -0,0 +1,30 @@ +## 2.3.3 + +* Update android gradle plugin to 7.3.1. + +## 2.3.2 + +* Update `com.google.android.gms:play-services-maps` to 18.1.0. + +## 2.3.1 + +* Updates imports for `prefer_relative_imports`. + +## 2.3.0 + +* Switches the default for `useAndroidViewSurface` to true, and adds + information about the current mode behaviors to the README. +* Updates minimum Flutter version to 2.10. + +## 2.2.0 + +* Updates `useAndroidViewSurface` to require Hybrid Composition, making the + selection work again in Flutter 3.0+. Earlier versions of Flutter are + no longer supported. +* Fixes violations of new analysis option use_named_constants. +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 2.1.10 + +* Splits Android implementation out of `google_maps_flutter` as a federated + implementation. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/LICENSE b/packages/google_maps_flutter/google_maps_flutter_android/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/README.md b/packages/google_maps_flutter/google_maps_flutter_android/README.md new file mode 100644 index 000000000000..877b9bbe9102 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/README.md @@ -0,0 +1,54 @@ +# google\_maps\_flutter\_android + + + +The Android implementation of [`google_maps_flutter`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use +`google_maps_flutter` normally. This package will be automatically included in +your app when you do. + +## Display Mode + +This plugin supports two different [platform view display modes][3]. The default +display mode is subject to change in the future, and will not be considered a +breaking change, so if you want to ensure a specific mode you can set it +explicitly: + + +```dart +import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + // Require Hybrid Composition mode on Android. + final GoogleMapsFlutterPlatform mapsImplementation = + GoogleMapsFlutterPlatform.instance; + if (mapsImplementation is GoogleMapsFlutterAndroid) { + mapsImplementation.useAndroidViewSurface = true; + } + // ··· +} +``` + +### Hybrid Composition + +This is the current default mode, and corresponds to +`useAndroidViewSurface = true`. It ensures that the map display will work as +expected, at the cost of some performance. + +### Texture Layer Hybrid Composition + +This is a new display mode used by most plugins starting with Flutter 3.0, and +corresponds to `useAndroidViewSurface = false`. This is more performant than +Hybrid Composition, but currently [misses certain map updates][4]. + +This mode will likely become the default in future versions if/when the +missed updates issue can be resolved. + +[1]: https://pub.dev/packages/google_maps_flutter +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[3]: https://docs.flutter.dev/development/platform-integration/android/platform-views +[4]: https://github.com/flutter/flutter/issues/103686 diff --git a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle similarity index 80% rename from packages/google_maps_flutter/google_maps_flutter/android/build.gradle rename to packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle index bf283bea9ef9..2d507c6cc9fa 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.3.1' } } @@ -29,18 +29,20 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { + // TODO(stuartmorgan): Enable when gradle is updated. + // disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' } dependencies { implementation "androidx.annotation:annotation:1.1.0" - implementation 'com.google.android.gms:play-services-maps:17.0.0' + implementation 'com.google.android.gms:play-services-maps:18.1.0' androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test:rules:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:3.2.4' + androidTestImplementation 'androidx.test:rules:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:4.7.0' 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/settings.gradle b/packages/google_maps_flutter/google_maps_flutter_android/android/settings.gradle new file mode 100644 index 000000000000..d873c7abe92c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'google_maps_flutter_android' diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/AndroidManifest.xml b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/AndroidManifest.xml rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/AndroidManifest.xml diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/CircleBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/CircleBuilder.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/CircleBuilder.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/CircleBuilder.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/CircleController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/CircleController.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/CircleController.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/CircleController.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/CircleOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/CircleOptionsSink.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/CircleOptionsSink.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/CircleOptionsSink.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/CirclesController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/CirclesController.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/CirclesController.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/CirclesController.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java similarity index 91% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index 9b8810354b8f..66d3e283b8df 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -12,9 +12,11 @@ import android.graphics.Point; import android.os.Bundle; import android.util.Log; +import android.view.Choreographer; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; @@ -94,7 +96,8 @@ final class GoogleMapController this.options = options; this.mapView = new MapView(context, options); this.density = context.getResources().getDisplayMetrics().density; - methodChannel = new MethodChannel(binaryMessenger, "plugins.flutter.io/google_maps_" + id); + methodChannel = + new MethodChannel(binaryMessenger, "plugins.flutter.dev/google_maps_android_" + id); methodChannel.setMethodCallHandler(this); this.lifecycleProvider = lifecycleProvider; this.markersController = new MarkersController(methodChannel); @@ -109,6 +112,11 @@ public View getView() { return mapView; } + @VisibleForTesting + /*package*/ void setView(MapView view) { + mapView = view; + } + void init() { lifecycleProvider.getLifecycle().addObserver(this); mapView.getMapAsync(this); @@ -126,6 +134,58 @@ private CameraPosition getCameraPosition() { return trackCameraPosition ? googleMap.getCameraPosition() : null; } + private boolean loadedCallbackPending = false; + + /** + * Invalidates the map view after the map has finished rendering. + * + *

gmscore GL renderer uses a {@link android.view.TextureView}. Android platform views that are + * displayed as a texture after Flutter v3.0.0. require that the view hierarchy is notified after + * all drawing operations have been flushed. + * + *

Since the GL renderer doesn't use standard Android views, and instead uses GL directly, we + * notify the view hierarchy by invalidating the view. + * + *

Unfortunately, when {@link GoogleMap.OnMapLoadedCallback} is fired, the texture may not have + * been updated yet. + * + *

To workaround this limitation, wait two frames. This ensures that at least the frame budget + * (16.66ms at 60hz) have passed since the drawing operation was issued. + */ + private void invalidateMapIfNeeded() { + if (googleMap == null || loadedCallbackPending) { + return; + } + loadedCallbackPending = true; + googleMap.setOnMapLoadedCallback( + new GoogleMap.OnMapLoadedCallback() { + @Override + public void onMapLoaded() { + loadedCallbackPending = false; + postFrameCallback( + () -> { + postFrameCallback( + () -> { + if (mapView != null) { + mapView.invalidate(); + } + }); + }); + } + }); + } + + private static void postFrameCallback(Runnable f) { + Choreographer.getInstance() + .postFrameCallback( + new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + f.run(); + } + }); + } + @Override public void onMapReady(GoogleMap googleMap) { this.googleMap = googleMap; @@ -244,6 +304,7 @@ public void onSnapshotReady(Bitmap bitmap) { } case "markers#update": { + invalidateMapIfNeeded(); List markersToAdd = call.argument("markersToAdd"); markersController.addMarkers(markersToAdd); List markersToChange = call.argument("markersToChange"); @@ -273,6 +334,7 @@ public void onSnapshotReady(Bitmap bitmap) { } case "polygons#update": { + invalidateMapIfNeeded(); List polygonsToAdd = call.argument("polygonsToAdd"); polygonsController.addPolygons(polygonsToAdd); List polygonsToChange = call.argument("polygonsToChange"); @@ -284,6 +346,7 @@ public void onSnapshotReady(Bitmap bitmap) { } case "polylines#update": { + invalidateMapIfNeeded(); List polylinesToAdd = call.argument("polylinesToAdd"); polylinesController.addPolylines(polylinesToAdd); List polylinesToChange = call.argument("polylinesToChange"); @@ -295,6 +358,7 @@ public void onSnapshotReady(Bitmap bitmap) { } case "circles#update": { + invalidateMapIfNeeded(); List circlesToAdd = call.argument("circlesToAdd"); circlesController.addCircles(circlesToAdd); List circlesToChange = call.argument("circlesToChange"); @@ -374,12 +438,17 @@ public void onSnapshotReady(Bitmap bitmap) { } case "map#setStyle": { - String mapStyle = (String) call.arguments; + invalidateMapIfNeeded(); boolean mapStyleSet; - if (mapStyle == null) { - mapStyleSet = googleMap.setMapStyle(null); + if (call.arguments instanceof String) { + String mapStyle = (String) call.arguments; + if (mapStyle == null) { + mapStyleSet = googleMap.setMapStyle(null); + } else { + mapStyleSet = googleMap.setMapStyle(new MapStyleOptions(mapStyle)); + } } else { - mapStyleSet = googleMap.setMapStyle(new MapStyleOptions(mapStyle)); + mapStyleSet = googleMap.setMapStyle(null); } ArrayList mapStyleResult = new ArrayList<>(2); mapStyleResult.add(mapStyleSet); @@ -392,6 +461,7 @@ public void onSnapshotReady(Bitmap bitmap) { } case "tileOverlays#update": { + invalidateMapIfNeeded(); List> tileOverlaysToAdd = call.argument("tileOverlaysToAdd"); tileOverlaysController.addTileOverlays(tileOverlaysToAdd); List> tileOverlaysToChange = call.argument("tileOverlaysToChange"); @@ -403,6 +473,7 @@ public void onSnapshotReady(Bitmap bitmap) { } case "tileOverlays#clearTileCache": { + invalidateMapIfNeeded(); String tileOverlayId = call.argument("tileOverlayId"); tileOverlaysController.clearTileCache(tileOverlayId); result.success(null); diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapListener.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapListener.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapListener.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapListener.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java similarity index 98% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java index 763cd9e3e72e..715b357566da 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java @@ -28,7 +28,7 @@ public class GoogleMapsPlugin implements FlutterPlugin, ActivityAware { @Nullable private Lifecycle lifecycle; - private static final String VIEW_TYPE = "plugins.flutter.io/google_maps"; + private static final String VIEW_TYPE = "plugins.flutter.dev/google_maps_android"; @SuppressWarnings("deprecation") public static void registerWith( diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/LifecycleProvider.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/LifecycleProvider.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/LifecycleProvider.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/LifecycleProvider.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerBuilder.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerBuilder.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerBuilder.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerOptionsSink.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerOptionsSink.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerOptionsSink.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolylineBuilder.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineBuilder.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolylineBuilder.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolylineController.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineController.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolylineController.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolylineOptionsSink.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineOptionsSink.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolylineOptionsSink.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/CircleBuilderTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/CircleBuilderTest.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/CircleBuilderTest.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/CircleBuilderTest.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/CircleControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/CircleControllerTest.java similarity index 90% rename from packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/CircleControllerTest.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/CircleControllerTest.java index 72a8cab626b5..064c8c3591eb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/CircleControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/CircleControllerTest.java @@ -7,7 +7,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import com.google.android.gms.internal.maps.zzh; +import com.google.android.gms.internal.maps.zzl; import com.google.android.gms.maps.model.Circle; import org.junit.Test; import org.mockito.Mockito; @@ -16,7 +16,7 @@ public class CircleControllerTest { @Test public void controller_SetsStrokeDensity() { - final zzh z = mock(zzh.class); + final zzl z = mock(zzl.class); final Circle circle = spy(new Circle(z)); final float density = 5; 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 new file mode 100644 index 000000000000..d8082b57e3db --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java @@ -0,0 +1,148 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +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.verify; + +import android.content.Context; +import android.os.Build; +import androidx.activity.ComponentActivity; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.MapView; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.P) +public class GoogleMapControllerTest { + + private Context context; + private ComponentActivity activity; + private GoogleMapController googleMapController; + + @Mock BinaryMessenger mockMessenger; + @Mock GoogleMap mockGoogleMap; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + context = ApplicationProvider.getApplicationContext(); + activity = Robolectric.setupActivity(ComponentActivity.class); + googleMapController = + new GoogleMapController(0, context, mockMessenger, activity::getLifecycle, null); + googleMapController.init(); + } + + @Test + public void DisposeReleaseTheMap() throws InterruptedException { + googleMapController.onMapReady(mockGoogleMap); + assertTrue(googleMapController != null); + googleMapController.dispose(); + assertNull(googleMapController.getView()); + } + + @Test + public void OnDestroyReleaseTheMap() throws InterruptedException { + googleMapController.onMapReady(mockGoogleMap); + assertTrue(googleMapController != null); + googleMapController.onDestroy(activity); + assertNull(googleMapController.getView()); + } + + @Test + public void InvalidateMapAfterMethodCalls() throws InterruptedException { + String[] methodsThatTriggerInvalidation = { + "markers#update", + "polygons#update", + "polylines#update", + "circles#update", + "map#setStyle", + "tileOverlays#update", + "tileOverlays#clearTileCache" + }; + + for (String methodName : methodsThatTriggerInvalidation) { + googleMapController = + new GoogleMapController(0, context, mockMessenger, activity::getLifecycle, null); + googleMapController.init(); + + mockGoogleMap = mock(GoogleMap.class); + googleMapController.onMapReady(mockGoogleMap); + + MethodChannel.Result result = mock(MethodChannel.Result.class); + System.out.println(methodName); + googleMapController.onMethodCall( + new MethodCall(methodName, new HashMap()), result); + + ArgumentCaptor argument = + ArgumentCaptor.forClass(GoogleMap.OnMapLoadedCallback.class); + verify(mockGoogleMap).setOnMapLoadedCallback(argument.capture()); + + MapView mapView = mock(MapView.class); + googleMapController.setView(mapView); + + verify(mapView, never()).invalidate(); + argument.getValue().onMapLoaded(); + verify(mapView).invalidate(); + } + } + + @Test + public void InvalidateMapOnceAfterMethodCall() throws InterruptedException { + googleMapController.onMapReady(mockGoogleMap); + + MethodChannel.Result result = mock(MethodChannel.Result.class); + googleMapController.onMethodCall( + new MethodCall("markers#update", new HashMap()), result); + googleMapController.onMethodCall( + new MethodCall("polygons#update", new HashMap()), result); + + ArgumentCaptor argument = + ArgumentCaptor.forClass(GoogleMap.OnMapLoadedCallback.class); + verify(mockGoogleMap).setOnMapLoadedCallback(argument.capture()); + + MapView mapView = mock(MapView.class); + googleMapController.setView(mapView); + + verify(mapView, never()).invalidate(); + argument.getValue().onMapLoaded(); + verify(mapView).invalidate(); + } + + @Test + public void MethodCalledAfterControllerIsDestroyed() throws InterruptedException { + googleMapController.onMapReady(mockGoogleMap); + MethodChannel.Result result = mock(MethodChannel.Result.class); + googleMapController.onMethodCall( + new MethodCall("markers#update", new HashMap()), result); + + ArgumentCaptor argument = + ArgumentCaptor.forClass(GoogleMap.OnMapLoadedCallback.class); + verify(mockGoogleMap).setOnMapLoadedCallback(argument.capture()); + + MapView mapView = mock(MapView.class); + googleMapController.setView(mapView); + googleMapController.onDestroy(activity); + + argument.getValue().onMapLoaded(); + verify(mapView, never()).invalidate(); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolygonBuilderTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolygonBuilderTest.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolygonBuilderTest.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolygonBuilderTest.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolygonControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolygonControllerTest.java similarity index 90% rename from packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolygonControllerTest.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolygonControllerTest.java index 29234b6adb42..271c63bdc25c 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolygonControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolygonControllerTest.java @@ -7,7 +7,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import com.google.android.gms.internal.maps.zzw; +import com.google.android.gms.internal.maps.zzad; import com.google.android.gms.maps.model.Polygon; import org.junit.Test; import org.mockito.Mockito; @@ -16,7 +16,7 @@ public class PolygonControllerTest { @Test public void controller_SetsStrokeDensity() { - final zzw z = mock(zzw.class); + final zzad z = mock(zzad.class); final Polygon polygon = spy(new Polygon(z)); final float density = 5; diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineBuilderTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolylineBuilderTest.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineBuilderTest.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolylineBuilderTest.java diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java similarity index 90% rename from packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java index bb7653aa2293..abb98627b35a 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java @@ -7,7 +7,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import com.google.android.gms.internal.maps.zzz; +import com.google.android.gms.internal.maps.zzag; import com.google.android.gms.maps.model.Polyline; import org.junit.Test; import org.mockito.Mockito; @@ -16,7 +16,7 @@ public class PolylineControllerTest { @Test public void controller_SetsStrokeDensity() { - final zzz z = mock(zzz.class); + final zzag z = mock(zzag.class); final Polyline polyline = spy(new Polyline(z)); final float density = 5; diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to packages/google_maps_flutter/google_maps_flutter_android/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/.metadata b/packages/google_maps_flutter/google_maps_flutter_android/example/.metadata new file mode 100644 index 000000000000..46e884ce48d1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 3ea4d06340a97a1e9d7cae97567c64e0569dcaa2 + channel: beta diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/README.md b/packages/google_maps_flutter/google_maps_flutter_android/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/build.gradle b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/build.gradle new file mode 100644 index 000000000000..f6d29f63fadc --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/build.gradle @@ -0,0 +1,73 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.googlemapsexample" + minSdkVersion 20 + targetSdkVersion 28 + multiDexEnabled true + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + defaultConfig { + manifestPlaceholders = [mapsApiKey: "$System.env.MAPS_API_KEY"] + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } + + dependencies { + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' + testImplementation 'com.google.android.gms:play-services-maps:17.0.0' + } +} + +flutter { + source '../..' +} diff --git a/packages/espresso/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 80% rename from packages/espresso/android/gradle/wrapper/gradle-wrapper.properties rename to packages/google_maps_flutter/google_maps_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 4751774dd352..29e413457635 100644 --- a/packages/espresso/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Tue Nov 26 13:04:21 PST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -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. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java rename to packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java new file mode 100644 index 000000000000..244a22b6c6c8 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class MainActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/debug/AndroidManifest.xml b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..9c1f83d3cec5 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/debug/java/io/flutter/plugins/googlemapsexample/GoogleMapsTestActivity.java b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/debug/java/io/flutter/plugins/googlemapsexample/GoogleMapsTestActivity.java new file mode 100644 index 000000000000..e183a7c75c4e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/debug/java/io/flutter/plugins/googlemapsexample/GoogleMapsTestActivity.java @@ -0,0 +1,20 @@ +// 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.googlemapsexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class GoogleMapsTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/AndroidManifest.xml b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..815074bfad96 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/values/styles.xml b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..00fa4417cfbe --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter_android/example/android/build.gradle new file mode 100644 index 000000000000..c21bff8e0a2f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.0.1' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} 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 new file mode 100644 index 000000000000..207beb63fb48 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true + diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..b8793d3c0d69 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/settings.gradle b/packages/google_maps_flutter/google_maps_flutter_android/example/android/settings.gradle new file mode 100644 index 000000000000..5a2f14fb18f6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/assets/2.0x/red_square.png b/packages/google_maps_flutter/google_maps_flutter_android/example/assets/2.0x/red_square.png new file mode 100644 index 000000000000..0f82237796bf Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_android/example/assets/2.0x/red_square.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/assets/3.0x/red_square.png b/packages/google_maps_flutter/google_maps_flutter_android/example/assets/3.0x/red_square.png new file mode 100644 index 000000000000..7e2739974e7b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_android/example/assets/3.0x/red_square.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/assets/night_mode.json b/packages/google_maps_flutter/google_maps_flutter_android/example/assets/night_mode.json new file mode 100644 index 000000000000..1f16e003a920 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/assets/night_mode.json @@ -0,0 +1,162 @@ +[ + { + "elementType": "geometry", + "stylers": [ + { + "color": "#242f3e" + } + ] + }, + { + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#746855" + } + ] + }, + { + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#242f3e" + } + ] + }, + { + "featureType": "administrative.locality", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#d59563" + } + ] + }, + { + "featureType": "poi", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#d59563" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "geometry", + "stylers": [ + { + "color": "#263c3f" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#6b9a76" + } + ] + }, + { + "featureType": "road", + "elementType": "geometry", + "stylers": [ + { + "color": "#38414e" + } + ] + }, + { + "featureType": "road", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#212a37" + } + ] + }, + { + "featureType": "road", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#9ca5b3" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [ + { + "color": "#746855" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#1f2835" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#f3d19c" + } + ] + }, + { + "featureType": "transit", + "elementType": "geometry", + "stylers": [ + { + "color": "#2f3948" + } + ] + }, + { + "featureType": "transit.station", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#d59563" + } + ] + }, + { + "featureType": "water", + "elementType": "geometry", + "stylers": [ + { + "color": "#17263c" + } + ] + }, + { + "featureType": "water", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#515c6d" + } + ] + }, + { + "featureType": "water", + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#17263c" + } + ] + } +] + diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/assets/red_square.png b/packages/google_maps_flutter/google_maps_flutter_android/example/assets/red_square.png new file mode 100644 index 000000000000..650a2dee711d Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_android/example/assets/red_square.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/build.excerpt.yaml b/packages/google_maps_flutter/google_maps_flutter_android/example/build.excerpt.yaml new file mode 100644 index 000000000000..e317efa11cb3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/build.excerpt.yaml @@ -0,0 +1,15 @@ +targets: + $default: + sources: + include: + - lib/** + # Some default includes that aren't really used here but will prevent + # false-negative warnings: + - $package$ + - lib/$lib$ + exclude: + - '**/.*/**' + - '**/build/**' + builders: + code_excerpter|code_excerpter: + enabled: true diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_test.dart new file mode 100644 index 000000000000..0945740b1e45 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_test.dart @@ -0,0 +1,1225 @@ +// 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'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; +import 'package:google_maps_flutter_example/example_google_map.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +const LatLng _kInitialMapCenter = LatLng(0, 0); +const double _kInitialZoomLevel = 5; +const CameraPosition _kInitialCameraPosition = + CameraPosition(target: _kInitialMapCenter, zoom: _kInitialZoomLevel); + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + GoogleMapsFlutterPlatform.instance.enableDebugInspection(); + + // Repeatedly checks an asynchronous value against a test condition, waiting + // on frame between each check, returing the value if it passes the predicate + // before [maxTries] is reached. + // + // Returns null if the predicate is never satisfied. + // + // This is useful for cases where the Maps SDK has some internally + // asynchronous operation that we don't have visibility into (e.g., native UI + // animations). + Future waitForValueMatchingPredicate(WidgetTester tester, + Future Function() getValue, bool Function(T) predicate, + {int maxTries = 100}) async { + for (int i = 0; i < maxTries; i++) { + final T value = await getValue(); + if (predicate(value)) { + return value; + } + await tester.pump(); + } + return null; + } + + testWidgets('uses surface view', (WidgetTester tester) async { + final GoogleMapsFlutterAndroid instance = + GoogleMapsFlutterPlatform.instance as GoogleMapsFlutterAndroid; + final bool previousUseAndroidViewSurfaceValue = + instance.useAndroidViewSurface; + instance.useAndroidViewSurface = true; + + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + compassEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + await mapIdCompleter.future; + + // Wait for the placeholder to be replaced by the actual view. + while (!tester.any(find.byType(AndroidViewSurface)) && + !tester.any(find.byType(AndroidView))) { + await tester.pump(); + } + + instance.useAndroidViewSurface = previousUseAndroidViewSurfaceValue; + + expect(tester.any(find.byType(AndroidViewSurface)), true); + }); + + testWidgets('testCompassToggle', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + compassEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool compassEnabled = await inspector.isCompassEnabled(mapId: mapId); + expect(compassEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + compassEnabled = await inspector.isCompassEnabled(mapId: mapId); + expect(compassEnabled, true); + }); + + testWidgets('testMapToolbarToggle', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + mapToolbarEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool mapToolbarEnabled = await inspector.isMapToolbarEnabled(mapId: mapId); + expect(mapToolbarEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + mapToolbarEnabled = await inspector.isMapToolbarEnabled(mapId: mapId); + expect(mapToolbarEnabled, true); + }); + + testWidgets('updateMinMaxZoomLevels', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + const MinMaxZoomPreference initialZoomLevel = MinMaxZoomPreference(4, 8); + const MinMaxZoomPreference finalZoomLevel = MinMaxZoomPreference(6, 10); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + minMaxZoomPreference: initialZoomLevel, + onMapCreated: (ExampleGoogleMapController c) async { + controllerCompleter.complete(c); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + // On Android, zooming with zoomTo is constrained by the min/max. + await controller.moveCamera(CameraUpdate.zoomTo(15)); + await tester.pumpAndSettle(); + double? zoomLevel = await controller.getZoomLevel(); + expect(zoomLevel, equals(initialZoomLevel.maxZoom)); + + await controller.moveCamera(CameraUpdate.zoomTo(1)); + await tester.pumpAndSettle(); + zoomLevel = await controller.getZoomLevel(); + expect(zoomLevel, equals(initialZoomLevel.minZoom)); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + minMaxZoomPreference: finalZoomLevel, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + await controller.moveCamera(CameraUpdate.zoomTo(15)); + await tester.pumpAndSettle(); + zoomLevel = await controller.getZoomLevel(); + expect(zoomLevel, equals(finalZoomLevel.maxZoom)); + + await controller.moveCamera(CameraUpdate.zoomTo(1)); + await tester.pumpAndSettle(); + zoomLevel = await controller.getZoomLevel(); + expect(zoomLevel, equals(finalZoomLevel.minZoom)); + }); + + testWidgets('testZoomGesturesEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + zoomGesturesEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool zoomGesturesEnabled = + await inspector.areZoomGesturesEnabled(mapId: mapId); + expect(zoomGesturesEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + zoomGesturesEnabled = await inspector.areZoomGesturesEnabled(mapId: mapId); + expect(zoomGesturesEnabled, true); + }); + + testWidgets('testZoomControlsEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool zoomControlsEnabled = + await inspector.areZoomControlsEnabled(mapId: mapId); + expect(zoomControlsEnabled, true); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + zoomControlsEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + zoomControlsEnabled = await inspector.areZoomControlsEnabled(mapId: mapId); + expect(zoomControlsEnabled, false); + }); + + testWidgets('testLiteModeEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool liteModeEnabled = await inspector.isLiteModeEnabled(mapId: mapId); + expect(liteModeEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + liteModeEnabled: true, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + liteModeEnabled = await inspector.isLiteModeEnabled(mapId: mapId); + expect(liteModeEnabled, true); + }); + + testWidgets('testRotateGesturesEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + rotateGesturesEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool rotateGesturesEnabled = + await inspector.areRotateGesturesEnabled(mapId: mapId); + expect(rotateGesturesEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + rotateGesturesEnabled = + await inspector.areRotateGesturesEnabled(mapId: mapId); + expect(rotateGesturesEnabled, true); + }); + + testWidgets('testTiltGesturesEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tiltGesturesEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool tiltGesturesEnabled = + await inspector.areTiltGesturesEnabled(mapId: mapId); + expect(tiltGesturesEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + tiltGesturesEnabled = await inspector.areTiltGesturesEnabled(mapId: mapId); + expect(tiltGesturesEnabled, true); + }); + + testWidgets('testScrollGesturesEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + scrollGesturesEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool scrollGesturesEnabled = + await inspector.areScrollGesturesEnabled(mapId: mapId); + expect(scrollGesturesEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + scrollGesturesEnabled = + await inspector.areScrollGesturesEnabled(mapId: mapId); + expect(scrollGesturesEnabled, true); + }); + + testWidgets('testInitialCenterLocationAtCenter', (WidgetTester tester) async { + await tester.binding.setSurfaceSize(const Size(800, 600)); + + final Completer mapControllerCompleter = + Completer(); + final Key key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapControllerCompleter.complete(controller); + }, + ), + ), + ); + final ExampleGoogleMapController mapController = + await mapControllerCompleter.future; + + await tester.pumpAndSettle(); + + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and + // `mapControllerCompleter.complete(controller)` above should happen in + // `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + final ScreenCoordinate coordinate = + await mapController.getScreenCoordinate(_kInitialCameraPosition.target); + final Rect rect = tester.getRect(find.byKey(key)); + expect( + coordinate.x, + ((rect.center.dx - rect.topLeft.dx) * + tester.binding.window.devicePixelRatio) + .round()); + expect( + coordinate.y, + ((rect.center.dy - rect.topLeft.dy) * + tester.binding.window.devicePixelRatio) + .round()); + await tester.binding.setSurfaceSize(null); + }); + + testWidgets('testGetVisibleRegion', (WidgetTester tester) async { + final Key key = GlobalKey(); + final LatLngBounds zeroLatLngBounds = LatLngBounds( + southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)); + + final Completer mapControllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapControllerCompleter.complete(controller); + }, + ), + )); + await tester.pumpAndSettle(); + + final ExampleGoogleMapController mapController = + await mapControllerCompleter.future; + + // Wait for the visible region to be non-zero. + final LatLngBounds firstVisibleRegion = + await waitForValueMatchingPredicate( + tester, + () => mapController.getVisibleRegion(), + (LatLngBounds bounds) => bounds != zeroLatLngBounds) ?? + zeroLatLngBounds; + expect(firstVisibleRegion, isNot(zeroLatLngBounds)); + expect(firstVisibleRegion.contains(_kInitialMapCenter), isTrue); + + // Making a new `LatLngBounds` about (10, 10) distance south west to the `firstVisibleRegion`. + // The size of the `LatLngBounds` is 10 by 10. + final LatLng southWest = LatLng(firstVisibleRegion.southwest.latitude - 20, + firstVisibleRegion.southwest.longitude - 20); + final LatLng northEast = LatLng(firstVisibleRegion.southwest.latitude - 10, + firstVisibleRegion.southwest.longitude - 10); + final LatLng newCenter = LatLng( + (northEast.latitude + southWest.latitude) / 2, + (northEast.longitude + southWest.longitude) / 2, + ); + + expect(firstVisibleRegion.contains(northEast), isFalse); + expect(firstVisibleRegion.contains(southWest), isFalse); + + final LatLngBounds latLngBounds = + LatLngBounds(southwest: southWest, northeast: northEast); + + // TODO(iskakaushik): non-zero padding is needed for some device configurations + // https://github.com/flutter/flutter/issues/30575 + const double padding = 0; + await mapController + .moveCamera(CameraUpdate.newLatLngBounds(latLngBounds, padding)); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final LatLngBounds secondVisibleRegion = + await mapController.getVisibleRegion(); + + expect(secondVisibleRegion, isNot(zeroLatLngBounds)); + + expect(firstVisibleRegion, isNot(secondVisibleRegion)); + expect(secondVisibleRegion.contains(newCenter), isTrue); + }); + + testWidgets('testTraffic', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + trafficEnabled: true, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool isTrafficEnabled = await inspector.isTrafficEnabled(mapId: mapId); + expect(isTrafficEnabled, true); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + isTrafficEnabled = await inspector.isTrafficEnabled(mapId: mapId); + expect(isTrafficEnabled, false); + }); + + testWidgets('testBuildings', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + final bool isBuildingsEnabled = + await inspector.areBuildingsEnabled(mapId: mapId); + expect(isBuildingsEnabled, true); + }); + + testWidgets('testMyLocationButtonToggle', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(mapId: mapId); + expect(myLocationButtonEnabled, true); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + myLocationButtonEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(mapId: mapId); + expect(myLocationButtonEnabled, false); + }, + // Location button tests are skipped in Android because we don't have location permission to test. + skip: true); + + testWidgets('testMyLocationButton initial value false', + (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + myLocationButtonEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + final bool myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(mapId: mapId); + expect(myLocationButtonEnabled, false); + }, + // Location button tests are skipped in Android because we don't have location permission to test. + skip: true); + + testWidgets('testMyLocationButton initial value true', + (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + final bool myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(mapId: mapId); + expect(myLocationButtonEnabled, true); + }, + // Location button tests are skipped in Android because we don't have location permission to test. + skip: true); + + testWidgets('testSetMapStyle valid Json String', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + const String mapStyle = + '[{"elementType":"geometry","stylers":[{"color":"#242f3e"}]}]'; + await controller.setMapStyle(mapStyle); + }); + + testWidgets('testSetMapStyle invalid Json String', + (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + try { + await controller.setMapStyle('invalid_value'); + fail('expected MapStyleException'); + } on MapStyleException catch (e) { + expect(e.cause, isNotNull); + } + }); + + testWidgets('testSetMapStyle null string', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + await controller.setMapStyle(null); + }); + + testWidgets('testGetLatLng', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + final LatLngBounds visibleRegion = await controller.getVisibleRegion(); + final LatLng topLeft = + await controller.getLatLng(const ScreenCoordinate(x: 0, y: 0)); + final LatLng northWest = LatLng( + visibleRegion.northeast.latitude, + visibleRegion.southwest.longitude, + ); + + expect(topLeft, northWest); + }); + + testWidgets('testGetZoomLevel', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + double zoom = await controller.getZoomLevel(); + expect(zoom, _kInitialZoomLevel); + + await controller.moveCamera(CameraUpdate.zoomTo(7)); + await tester.pumpAndSettle(); + zoom = await controller.getZoomLevel(); + expect(zoom, equals(7)); + }); + + testWidgets('testScreenCoordinate', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + final LatLngBounds visibleRegion = await controller.getVisibleRegion(); + final LatLng northWest = LatLng( + visibleRegion.northeast.latitude, + visibleRegion.southwest.longitude, + ); + final ScreenCoordinate topLeft = + await controller.getScreenCoordinate(northWest); + expect(topLeft, const ScreenCoordinate(x: 0, y: 0)); + }); + + testWidgets('testResizeWidget', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final ExampleGoogleMap map = ExampleGoogleMap( + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) async { + controllerCompleter.complete(controller); + }, + ); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: MaterialApp( + home: Scaffold( + body: SizedBox(height: 100, width: 100, child: map))))); + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: MaterialApp( + home: Scaffold( + body: SizedBox(height: 400, width: 400, child: map))))); + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + // Simple call to make sure that the app hasn't crashed. + final LatLngBounds bounds1 = await controller.getVisibleRegion(); + final LatLngBounds bounds2 = await controller.getVisibleRegion(); + expect(bounds1, bounds2); + }); + + testWidgets('testToggleInfoWindow', (WidgetTester tester) async { + const Marker marker = Marker( + markerId: MarkerId('marker'), + infoWindow: InfoWindow(title: 'InfoWindow')); + final Set markers = {marker}; + + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + markers: markers, + onMapCreated: (ExampleGoogleMapController googleMapController) { + controllerCompleter.complete(googleMapController); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + bool iwVisibleStatus = + await controller.isMarkerInfoWindowShown(marker.markerId); + expect(iwVisibleStatus, false); + + await controller.showMarkerInfoWindow(marker.markerId); + // The Maps SDK doesn't always return true for whether it is shown + // immediately after showing it, so wait for it to report as shown. + iwVisibleStatus = await waitForValueMatchingPredicate( + tester, + () => controller.isMarkerInfoWindowShown(marker.markerId), + (bool visible) => visible) ?? + false; + expect(iwVisibleStatus, true); + + await controller.hideMarkerInfoWindow(marker.markerId); + iwVisibleStatus = await controller.isMarkerInfoWindowShown(marker.markerId); + expect(iwVisibleStatus, false); + }); + + testWidgets('fromAssetImage', (WidgetTester tester) async { + const double pixelRatio = 2; + const ImageConfiguration imageConfiguration = + ImageConfiguration(devicePixelRatio: pixelRatio); + final BitmapDescriptor mip = await BitmapDescriptor.fromAssetImage( + imageConfiguration, 'red_square.png'); + final BitmapDescriptor scaled = await BitmapDescriptor.fromAssetImage( + imageConfiguration, 'red_square.png', + mipmaps: false); + expect((mip.toJson() as List)[2], 1); + expect((scaled.toJson() as List)[2], 2); + }); + + testWidgets('testTakeSnapshot', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + final Uint8List? bytes = await controller.takeSnapshot(); + expect(bytes?.isNotEmpty, true); + }, + // TODO(cyanglaz): un-skip the test when we can test this on CI with API key enabled. + // https://github.com/flutter/flutter/issues/57057 + skip: Platform.isAndroid); + + testWidgets( + 'set tileOverlay correctly', + (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 2, + transparency: 0.2, + ); + + final TileOverlay tileOverlay2 = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_2'), + tileProvider: _DebugTileProvider(), + zIndex: 1, + visible: false, + transparency: 0.3, + fadeIn: false, + ); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1, tileOverlay2}, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + final TileOverlay tileOverlayInfo1 = (await inspector + .getTileOverlayInfo(tileOverlay1.mapsId, mapId: mapId))!; + final TileOverlay tileOverlayInfo2 = (await inspector + .getTileOverlayInfo(tileOverlay2.mapsId, mapId: mapId))!; + + expect(tileOverlayInfo1.visible, isTrue); + expect(tileOverlayInfo1.fadeIn, isTrue); + expect( + tileOverlayInfo1.transparency, moreOrLessEquals(0.2, epsilon: 0.001)); + expect(tileOverlayInfo1.zIndex, 2); + + expect(tileOverlayInfo2.visible, isFalse); + expect(tileOverlayInfo2.fadeIn, isFalse); + expect( + tileOverlayInfo2.transparency, moreOrLessEquals(0.3, epsilon: 0.001)); + expect(tileOverlayInfo2.zIndex, 1); + }, + ); + + testWidgets( + 'update tileOverlays correctly', + (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Key key = GlobalKey(); + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 2, + transparency: 0.2, + ); + + final TileOverlay tileOverlay2 = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_2'), + tileProvider: _DebugTileProvider(), + zIndex: 3, + transparency: 0.5, + ); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1, tileOverlay2}, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + final TileOverlay tileOverlay1New = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 1, + visible: false, + transparency: 0.3, + fadeIn: false, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1New}, + onMapCreated: (ExampleGoogleMapController controller) { + fail('update: OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final TileOverlay tileOverlayInfo1 = (await inspector + .getTileOverlayInfo(tileOverlay1.mapsId, mapId: mapId))!; + final TileOverlay? tileOverlayInfo2 = + await inspector.getTileOverlayInfo(tileOverlay2.mapsId, mapId: mapId); + + expect(tileOverlayInfo1.visible, isFalse); + expect(tileOverlayInfo1.fadeIn, isFalse); + expect( + tileOverlayInfo1.transparency, moreOrLessEquals(0.3, epsilon: 0.001)); + expect(tileOverlayInfo1.zIndex, 1); + + expect(tileOverlayInfo2, isNull); + }, + ); + + testWidgets( + 'remove tileOverlays correctly', + (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Key key = GlobalKey(); + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 2, + transparency: 0.2, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1}, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + final TileOverlay? tileOverlayInfo1 = + await inspector.getTileOverlayInfo(tileOverlay1.mapsId, mapId: mapId); + + expect(tileOverlayInfo1, isNull); + }, + ); +} + +class _DebugTileProvider implements TileProvider { + _DebugTileProvider() { + boxPaint.isAntiAlias = true; + boxPaint.color = Colors.blue; + boxPaint.strokeWidth = 2.0; + boxPaint.style = PaintingStyle.stroke; + } + + static const int width = 100; + static const int height = 100; + static final Paint boxPaint = Paint(); + static const TextStyle textStyle = TextStyle( + color: Colors.red, + fontSize: 20, + ); + + @override + Future getTile(int x, int y, int? zoom) async { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final TextSpan textSpan = TextSpan( + text: '$x,$y', + style: textStyle, + ); + final TextPainter textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + ); + textPainter.layout( + maxWidth: width.toDouble(), + ); + textPainter.paint(canvas, Offset.zero); + canvas.drawRect( + Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint); + final ui.Picture picture = recorder.endRecording(); + final Uint8List byteData = await picture + .toImage(width, height) + .then((ui.Image image) => + image.toByteData(format: ui.ImageByteFormat.png)) + .then((ByteData? byteData) => byteData!.buffer.asUint8List()); + return Tile(width, height, byteData); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/animate_camera.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/animate_camera.dart new file mode 100644 index 000000000000..c34a3ba4b2fe --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/animate_camera.dart @@ -0,0 +1,171 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class AnimateCameraPage extends GoogleMapExampleAppPage { + const AnimateCameraPage({Key? key}) + : super(const Icon(Icons.map), 'Camera control, animated', key: key); + + @override + Widget build(BuildContext context) { + return const AnimateCamera(); + } +} + +class AnimateCamera extends StatefulWidget { + const AnimateCamera({Key? key}) : super(key: key); + @override + State createState() => AnimateCameraState(); +} + +class AnimateCameraState extends State { + ExampleGoogleMapController? mapController; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + mapController = controller; + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: ExampleGoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: + const CameraPosition(target: LatLng(0.0, 0.0)), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.newCameraPosition( + const CameraPosition( + bearing: 270.0, + target: LatLng(51.5160895, -0.1294527), + tilt: 30.0, + zoom: 17.0, + ), + ), + ); + }, + child: const Text('newCameraPosition'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.newLatLng( + const LatLng(56.1725505, 10.1850512), + ), + ); + }, + child: const Text('newLatLng'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.newLatLngBounds( + LatLngBounds( + southwest: const LatLng(-38.483935, 113.248673), + northeast: const LatLng(-8.982446, 153.823821), + ), + 10.0, + ), + ); + }, + child: const Text('newLatLngBounds'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.newLatLngZoom( + const LatLng(37.4231613, -122.087159), + 11.0, + ), + ); + }, + child: const Text('newLatLngZoom'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.scrollBy(150.0, -225.0), + ); + }, + child: const Text('scrollBy'), + ), + ], + ), + Column( + children: [ + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.zoomBy( + -0.5, + const Offset(30.0, 20.0), + ), + ); + }, + child: const Text('zoomBy with focus'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.zoomBy(-0.5), + ); + }, + child: const Text('zoomBy'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.zoomIn(), + ); + }, + child: const Text('zoomIn'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.zoomOut(), + ); + }, + child: const Text('zoomOut'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.zoomTo(16.0), + ); + }, + child: const Text('zoomTo'), + ), + ], + ), + ], + ) + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart new file mode 100644 index 000000000000..1c1261cb5b82 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart @@ -0,0 +1,538 @@ +// 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(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:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +// This is a pared down version of the Dart code from the app-facing package, +// to allow running the same examples for package-local testing. +// TODO(stuartmorgan): Consider extracting this to a shared package. See also +// https://github.com/flutter/flutter/issues/46716. + +/// Controller for a single ExampleGoogleMap instance running on the host platform. +class ExampleGoogleMapController { + ExampleGoogleMapController._( + this._googleMapState, { + required this.mapId, + }) { + _connectStreams(mapId); + } + + /// The mapId for this controller + final int mapId; + + /// Initialize control of a [ExampleGoogleMap] with [id]. + /// + /// Mainly for internal use when instantiating a [ExampleGoogleMapController] passed + /// in [ExampleGoogleMap.onMapCreated] callback. + static Future _init( + int id, + CameraPosition initialCameraPosition, + _ExampleGoogleMapState googleMapState, + ) async { + await GoogleMapsFlutterPlatform.instance.init(id); + return ExampleGoogleMapController._( + googleMapState, + mapId: id, + ); + } + + final _ExampleGoogleMapState _googleMapState; + + void _connectStreams(int mapId) { + if (_googleMapState.widget.onCameraMoveStarted != null) { + GoogleMapsFlutterPlatform.instance + .onCameraMoveStarted(mapId: mapId) + .listen((_) => _googleMapState.widget.onCameraMoveStarted!()); + } + if (_googleMapState.widget.onCameraMove != null) { + GoogleMapsFlutterPlatform.instance.onCameraMove(mapId: mapId).listen( + (CameraMoveEvent e) => _googleMapState.widget.onCameraMove!(e.value)); + } + if (_googleMapState.widget.onCameraIdle != null) { + GoogleMapsFlutterPlatform.instance + .onCameraIdle(mapId: mapId) + .listen((_) => _googleMapState.widget.onCameraIdle!()); + } + GoogleMapsFlutterPlatform.instance + .onMarkerTap(mapId: mapId) + .listen((MarkerTapEvent e) => _googleMapState.onMarkerTap(e.value)); + GoogleMapsFlutterPlatform.instance.onMarkerDragStart(mapId: mapId).listen( + (MarkerDragStartEvent e) => + _googleMapState.onMarkerDragStart(e.value, e.position)); + GoogleMapsFlutterPlatform.instance.onMarkerDrag(mapId: mapId).listen( + (MarkerDragEvent e) => + _googleMapState.onMarkerDrag(e.value, e.position)); + GoogleMapsFlutterPlatform.instance.onMarkerDragEnd(mapId: mapId).listen( + (MarkerDragEndEvent e) => + _googleMapState.onMarkerDragEnd(e.value, e.position)); + GoogleMapsFlutterPlatform.instance.onInfoWindowTap(mapId: mapId).listen( + (InfoWindowTapEvent e) => _googleMapState.onInfoWindowTap(e.value)); + GoogleMapsFlutterPlatform.instance + .onPolylineTap(mapId: mapId) + .listen((PolylineTapEvent e) => _googleMapState.onPolylineTap(e.value)); + GoogleMapsFlutterPlatform.instance + .onPolygonTap(mapId: mapId) + .listen((PolygonTapEvent e) => _googleMapState.onPolygonTap(e.value)); + GoogleMapsFlutterPlatform.instance + .onCircleTap(mapId: mapId) + .listen((CircleTapEvent e) => _googleMapState.onCircleTap(e.value)); + GoogleMapsFlutterPlatform.instance + .onTap(mapId: mapId) + .listen((MapTapEvent e) => _googleMapState.onTap(e.position)); + GoogleMapsFlutterPlatform.instance.onLongPress(mapId: mapId).listen( + (MapLongPressEvent e) => _googleMapState.onLongPress(e.position)); + } + + /// Updates configuration options of the map user interface. + Future _updateMapConfiguration(MapConfiguration update) { + return GoogleMapsFlutterPlatform.instance + .updateMapConfiguration(update, mapId: mapId); + } + + /// Updates marker configuration. + Future _updateMarkers(MarkerUpdates markerUpdates) { + return GoogleMapsFlutterPlatform.instance + .updateMarkers(markerUpdates, mapId: mapId); + } + + /// Updates polygon configuration. + Future _updatePolygons(PolygonUpdates polygonUpdates) { + return GoogleMapsFlutterPlatform.instance + .updatePolygons(polygonUpdates, mapId: mapId); + } + + /// Updates polyline configuration. + Future _updatePolylines(PolylineUpdates polylineUpdates) { + return GoogleMapsFlutterPlatform.instance + .updatePolylines(polylineUpdates, mapId: mapId); + } + + /// Updates circle configuration. + Future _updateCircles(CircleUpdates circleUpdates) { + return GoogleMapsFlutterPlatform.instance + .updateCircles(circleUpdates, mapId: mapId); + } + + /// Updates tile overlays configuration. + Future _updateTileOverlays(Set newTileOverlays) { + return GoogleMapsFlutterPlatform.instance + .updateTileOverlays(newTileOverlays: newTileOverlays, mapId: mapId); + } + + /// Clears the tile cache so that all tiles will be requested again from the + /// [TileProvider]. + Future clearTileCache(TileOverlayId tileOverlayId) async { + return GoogleMapsFlutterPlatform.instance + .clearTileCache(tileOverlayId, mapId: mapId); + } + + /// Starts an animated change of the map camera position. + Future animateCamera(CameraUpdate cameraUpdate) { + return GoogleMapsFlutterPlatform.instance + .animateCamera(cameraUpdate, mapId: mapId); + } + + /// Changes the map camera position. + Future moveCamera(CameraUpdate cameraUpdate) { + return GoogleMapsFlutterPlatform.instance + .moveCamera(cameraUpdate, mapId: mapId); + } + + /// Sets the styling of the base map. + Future setMapStyle(String? mapStyle) { + return GoogleMapsFlutterPlatform.instance + .setMapStyle(mapStyle, mapId: mapId); + } + + /// Return [LatLngBounds] defining the region that is visible in a map. + Future getVisibleRegion() { + return GoogleMapsFlutterPlatform.instance.getVisibleRegion(mapId: mapId); + } + + /// Return [ScreenCoordinate] of the [LatLng] in the current map view. + Future getScreenCoordinate(LatLng latLng) { + return GoogleMapsFlutterPlatform.instance + .getScreenCoordinate(latLng, mapId: mapId); + } + + /// Returns [LatLng] corresponding to the [ScreenCoordinate] in the current map view. + Future getLatLng(ScreenCoordinate screenCoordinate) { + return GoogleMapsFlutterPlatform.instance + .getLatLng(screenCoordinate, mapId: mapId); + } + + /// Programmatically show the Info Window for a [Marker]. + Future showMarkerInfoWindow(MarkerId markerId) { + return GoogleMapsFlutterPlatform.instance + .showMarkerInfoWindow(markerId, mapId: mapId); + } + + /// Programmatically hide the Info Window for a [Marker]. + Future hideMarkerInfoWindow(MarkerId markerId) { + return GoogleMapsFlutterPlatform.instance + .hideMarkerInfoWindow(markerId, mapId: mapId); + } + + /// Returns `true` when the [InfoWindow] is showing, `false` otherwise. + Future isMarkerInfoWindowShown(MarkerId markerId) { + return GoogleMapsFlutterPlatform.instance + .isMarkerInfoWindowShown(markerId, mapId: mapId); + } + + /// Returns the current zoom level of the map + Future getZoomLevel() { + return GoogleMapsFlutterPlatform.instance.getZoomLevel(mapId: mapId); + } + + /// Returns the image bytes of the map + Future takeSnapshot() { + return GoogleMapsFlutterPlatform.instance.takeSnapshot(mapId: mapId); + } + + /// Disposes of the platform resources + void dispose() { + GoogleMapsFlutterPlatform.instance.dispose(mapId: mapId); + } +} + +// The next map ID to create. +int _nextMapCreationId = 0; + +/// A widget which displays a map with data obtained from the Google Maps service. +class ExampleGoogleMap extends StatefulWidget { + /// Creates a widget displaying data from Google Maps services. + /// + /// [AssertionError] will be thrown if [initialCameraPosition] is null; + const ExampleGoogleMap({ + Key? key, + required this.initialCameraPosition, + this.onMapCreated, + this.gestureRecognizers = const >{}, + this.compassEnabled = true, + this.mapToolbarEnabled = true, + this.cameraTargetBounds = CameraTargetBounds.unbounded, + this.mapType = MapType.normal, + this.minMaxZoomPreference = MinMaxZoomPreference.unbounded, + this.rotateGesturesEnabled = true, + this.scrollGesturesEnabled = true, + this.zoomControlsEnabled = true, + this.zoomGesturesEnabled = true, + this.liteModeEnabled = false, + this.tiltGesturesEnabled = true, + this.myLocationEnabled = false, + this.myLocationButtonEnabled = true, + this.layoutDirection, + + /// If no padding is specified default padding will be 0. + this.padding = EdgeInsets.zero, + this.indoorViewEnabled = false, + this.trafficEnabled = false, + this.buildingsEnabled = true, + this.markers = const {}, + this.polygons = const {}, + this.polylines = const {}, + this.circles = const {}, + this.onCameraMoveStarted, + this.tileOverlays = const {}, + this.onCameraMove, + this.onCameraIdle, + this.onTap, + this.onLongPress, + }) : super(key: key); + + /// Callback method for when the map is ready to be used. + /// + /// Used to receive a [ExampleGoogleMapController] for this [ExampleGoogleMap]. + final void Function(ExampleGoogleMapController controller)? onMapCreated; + + /// The initial position of the map's camera. + final CameraPosition initialCameraPosition; + + /// True if the map should show a compass when rotated. + final bool compassEnabled; + + /// True if the map should show a toolbar when you interact with the map. Android only. + final bool mapToolbarEnabled; + + /// Geographical bounding box for the camera target. + final CameraTargetBounds cameraTargetBounds; + + /// Type of map tiles to be rendered. + final MapType mapType; + + /// The layout direction to use for the embedded view. + final TextDirection? layoutDirection; + + /// Preferred bounds for the camera zoom level. + /// + /// Actual bounds depend on map data and device. + final MinMaxZoomPreference minMaxZoomPreference; + + /// True if the map view should respond to rotate gestures. + final bool rotateGesturesEnabled; + + /// True if the map view should respond to scroll gestures. + final bool scrollGesturesEnabled; + + /// True if the map view should show zoom controls. This includes two buttons + /// to zoom in and zoom out. The default value is to show zoom controls. + final bool zoomControlsEnabled; + + /// True if the map view should respond to zoom gestures. + final bool zoomGesturesEnabled; + + /// True if the map view should be in lite mode. Android only. + final bool liteModeEnabled; + + /// True if the map view should respond to tilt gestures. + final bool tiltGesturesEnabled; + + /// Padding to be set on map. + final EdgeInsets padding; + + /// Markers to be placed on the map. + final Set markers; + + /// Polygons to be placed on the map. + final Set polygons; + + /// Polylines to be placed on the map. + final Set polylines; + + /// Circles to be placed on the map. + final Set circles; + + /// Tile overlays to be placed on the map. + final Set tileOverlays; + + /// Called when the camera starts moving. + final VoidCallback? onCameraMoveStarted; + + /// Called repeatedly as the camera continues to move after an + /// onCameraMoveStarted call. + final CameraPositionCallback? onCameraMove; + + /// Called when camera movement has ended, there are no pending + /// animations and the user has stopped interacting with the map. + final VoidCallback? onCameraIdle; + + /// Called every time a [ExampleGoogleMap] is tapped. + final ArgumentCallback? onTap; + + /// Called every time a [ExampleGoogleMap] is long pressed. + final ArgumentCallback? onLongPress; + + /// True if a "My Location" layer should be shown on the map. + final bool myLocationEnabled; + + /// Enables or disables the my-location button. + final bool myLocationButtonEnabled; + + /// Enables or disables the indoor view from the map + final bool indoorViewEnabled; + + /// Enables or disables the traffic layer of the map + final bool trafficEnabled; + + /// Enables or disables showing 3D buildings where available + final bool buildingsEnabled; + + /// Which gestures should be consumed by the map. + final Set> gestureRecognizers; + + /// Creates a [State] for this [ExampleGoogleMap]. + @override + State createState() => _ExampleGoogleMapState(); +} + +class _ExampleGoogleMapState extends State { + final int _mapId = _nextMapCreationId++; + + final Completer _controller = + Completer(); + + Map _markers = {}; + Map _polygons = {}; + Map _polylines = {}; + Map _circles = {}; + late MapConfiguration _mapConfiguration; + + @override + Widget build(BuildContext context) { + return GoogleMapsFlutterPlatform.instance.buildViewWithConfiguration( + _mapId, + onPlatformViewCreated, + widgetConfiguration: MapWidgetConfiguration( + textDirection: widget.layoutDirection ?? + Directionality.maybeOf(context) ?? + TextDirection.ltr, + initialCameraPosition: widget.initialCameraPosition, + gestureRecognizers: widget.gestureRecognizers, + ), + mapObjects: MapObjects( + markers: widget.markers, + polygons: widget.polygons, + polylines: widget.polylines, + circles: widget.circles, + ), + mapConfiguration: _mapConfiguration, + ); + } + + @override + void initState() { + super.initState(); + _mapConfiguration = _configurationFromMapWidget(widget); + _markers = keyByMarkerId(widget.markers); + _polygons = keyByPolygonId(widget.polygons); + _polylines = keyByPolylineId(widget.polylines); + _circles = keyByCircleId(widget.circles); + } + + @override + void dispose() { + _controller.future + .then((ExampleGoogleMapController controller) => controller.dispose()); + super.dispose(); + } + + @override + void didUpdateWidget(ExampleGoogleMap oldWidget) { + super.didUpdateWidget(oldWidget); + _updateOptions(); + _updateMarkers(); + _updatePolygons(); + _updatePolylines(); + _updateCircles(); + _updateTileOverlays(); + } + + Future _updateOptions() async { + final MapConfiguration newConfig = _configurationFromMapWidget(widget); + final MapConfiguration updates = newConfig.diffFrom(_mapConfiguration); + if (updates.isEmpty) { + return; + } + final ExampleGoogleMapController controller = await _controller.future; + controller._updateMapConfiguration(updates); + _mapConfiguration = newConfig; + } + + Future _updateMarkers() async { + final ExampleGoogleMapController controller = await _controller.future; + controller._updateMarkers( + MarkerUpdates.from(_markers.values.toSet(), widget.markers)); + _markers = keyByMarkerId(widget.markers); + } + + Future _updatePolygons() async { + final ExampleGoogleMapController controller = await _controller.future; + controller._updatePolygons( + PolygonUpdates.from(_polygons.values.toSet(), widget.polygons)); + _polygons = keyByPolygonId(widget.polygons); + } + + Future _updatePolylines() async { + final ExampleGoogleMapController controller = await _controller.future; + controller._updatePolylines( + PolylineUpdates.from(_polylines.values.toSet(), widget.polylines)); + _polylines = keyByPolylineId(widget.polylines); + } + + Future _updateCircles() async { + final ExampleGoogleMapController controller = await _controller.future; + controller._updateCircles( + CircleUpdates.from(_circles.values.toSet(), widget.circles)); + _circles = keyByCircleId(widget.circles); + } + + Future _updateTileOverlays() async { + final ExampleGoogleMapController controller = await _controller.future; + controller._updateTileOverlays(widget.tileOverlays); + } + + Future onPlatformViewCreated(int id) async { + final ExampleGoogleMapController controller = + await ExampleGoogleMapController._init( + id, + widget.initialCameraPosition, + this, + ); + _controller.complete(controller); + _updateTileOverlays(); + widget.onMapCreated?.call(controller); + } + + void onMarkerTap(MarkerId markerId) { + _markers[markerId]!.onTap?.call(); + } + + void onMarkerDragStart(MarkerId markerId, LatLng position) { + _markers[markerId]!.onDragStart?.call(position); + } + + void onMarkerDrag(MarkerId markerId, LatLng position) { + _markers[markerId]!.onDrag?.call(position); + } + + void onMarkerDragEnd(MarkerId markerId, LatLng position) { + _markers[markerId]!.onDragEnd?.call(position); + } + + void onPolygonTap(PolygonId polygonId) { + _polygons[polygonId]!.onTap?.call(); + } + + void onPolylineTap(PolylineId polylineId) { + _polylines[polylineId]!.onTap?.call(); + } + + void onCircleTap(CircleId circleId) { + _circles[circleId]!.onTap?.call(); + } + + void onInfoWindowTap(MarkerId markerId) { + _markers[markerId]!.infoWindow.onTap?.call(); + } + + void onTap(LatLng position) { + widget.onTap?.call(position); + } + + void onLongPress(LatLng position) { + widget.onLongPress?.call(position); + } +} + +/// Builds a [MapConfiguration] from the given [map]. +MapConfiguration _configurationFromMapWidget(ExampleGoogleMap map) { + return MapConfiguration( + compassEnabled: map.compassEnabled, + mapToolbarEnabled: map.mapToolbarEnabled, + cameraTargetBounds: map.cameraTargetBounds, + mapType: map.mapType, + minMaxZoomPreference: map.minMaxZoomPreference, + rotateGesturesEnabled: map.rotateGesturesEnabled, + scrollGesturesEnabled: map.scrollGesturesEnabled, + tiltGesturesEnabled: map.tiltGesturesEnabled, + trackCameraPosition: map.onCameraMove != null, + zoomControlsEnabled: map.zoomControlsEnabled, + zoomGesturesEnabled: map.zoomGesturesEnabled, + liteModeEnabled: map.liteModeEnabled, + myLocationEnabled: map.myLocationEnabled, + myLocationButtonEnabled: map.myLocationButtonEnabled, + padding: map.padding, + indoorViewEnabled: map.indoorViewEnabled, + trafficEnabled: map.trafficEnabled, + buildingsEnabled: map.buildingsEnabled, + ); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/lite_mode.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/lite_mode.dart new file mode 100644 index 000000000000..f7bead951f5d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/lite_mode.dart @@ -0,0 +1,47 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +const CameraPosition _kInitialPosition = + CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); + +class LiteModePage extends GoogleMapExampleAppPage { + const LiteModePage({Key? key}) + : super(const Icon(Icons.map), 'Lite mode', key: key); + + @override + Widget build(BuildContext context) { + return const _LiteModeBody(); + } +} + +class _LiteModeBody extends StatelessWidget { + const _LiteModeBody(); + + @override + Widget build(BuildContext context) { + return const Card( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 30.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: _kInitialPosition, + liteModeEnabled: true, + ), + ), + ), + ), + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/main.dart new file mode 100644 index 000000000000..4adec524f87b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/main.dart @@ -0,0 +1,78 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'animate_camera.dart'; +import 'lite_mode.dart'; +import 'map_click.dart'; +import 'map_coordinates.dart'; +import 'map_ui.dart'; +import 'marker_icons.dart'; +import 'move_camera.dart'; +import 'padding.dart'; +import 'page.dart'; +import 'place_circle.dart'; +import 'place_marker.dart'; +import 'place_polygon.dart'; +import 'place_polyline.dart'; +import 'scrolling_map.dart'; +import 'snapshot.dart'; +import 'tile_overlay.dart'; + +final List _allPages = [ + const MapUiPage(), + const MapCoordinatesPage(), + const MapClickPage(), + const AnimateCameraPage(), + const MoveCameraPage(), + const PlaceMarkerPage(), + const MarkerIconsPage(), + const ScrollingMapPage(), + const PlacePolylinePage(), + const PlacePolygonPage(), + const PlaceCirclePage(), + const PaddingPage(), + const SnapshotPage(), + const LiteModePage(), + const TileOverlayPage(), +]; + +/// MapsDemo is the Main Application. +class MapsDemo extends StatelessWidget { + /// Default Constructor + const MapsDemo({Key? key}) : super(key: key); + + void _pushPage(BuildContext context, GoogleMapExampleAppPage page) { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => Scaffold( + appBar: AppBar(title: Text(page.title)), + body: page, + ))); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('GoogleMaps examples')), + body: ListView.builder( + itemCount: _allPages.length, + itemBuilder: (_, int index) => ListTile( + leading: _allPages[index].leading, + title: Text(_allPages[index].title), + onTap: () => _pushPage(context, _allPages[index]), + ), + ), + ); + } +} + +void main() { + final GoogleMapsFlutterPlatform platform = GoogleMapsFlutterPlatform.instance; + // Default to Hybrid Composition for the example. + (platform as GoogleMapsFlutterAndroid).useAndroidViewSurface = true; + runApp(const MaterialApp(home: MapsDemo())); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_click.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_click.dart new file mode 100644 index 000000000000..4017a9fccce2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_click.dart @@ -0,0 +1,105 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +const CameraPosition _kInitialPosition = + CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); + +class MapClickPage extends GoogleMapExampleAppPage { + const MapClickPage({Key? key}) + : super(const Icon(Icons.mouse), 'Map click', key: key); + + @override + Widget build(BuildContext context) { + return const _MapClickBody(); + } +} + +class _MapClickBody extends StatefulWidget { + const _MapClickBody(); + + @override + State createState() => _MapClickBodyState(); +} + +class _MapClickBodyState extends State<_MapClickBody> { + _MapClickBodyState(); + + ExampleGoogleMapController? mapController; + LatLng? _lastTap; + LatLng? _lastLongPress; + + @override + Widget build(BuildContext context) { + final ExampleGoogleMap googleMap = ExampleGoogleMap( + onMapCreated: onMapCreated, + initialCameraPosition: _kInitialPosition, + onTap: (LatLng pos) { + setState(() { + _lastTap = pos; + }); + }, + onLongPress: (LatLng pos) { + setState(() { + _lastLongPress = pos; + }); + }, + ); + + final List columnChildren = [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: googleMap, + ), + ), + ), + ]; + + if (mapController != null) { + final String lastTap = 'Tap:\n${_lastTap ?? ""}\n'; + final String lastLongPress = 'Long press:\n${_lastLongPress ?? ""}'; + columnChildren.add(Center( + child: Text( + lastTap, + textAlign: TextAlign.center, + ))); + columnChildren.add(Center( + child: Text( + _lastTap != null ? 'Tapped' : '', + textAlign: TextAlign.center, + ))); + columnChildren.add(Center( + child: Text( + lastLongPress, + textAlign: TextAlign.center, + ))); + columnChildren.add(Center( + child: Text( + _lastLongPress != null ? 'Long pressed' : '', + textAlign: TextAlign.center, + ))); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: columnChildren, + ); + } + + Future onMapCreated(ExampleGoogleMapController controller) async { + setState(() { + mapController = controller; + }); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_coordinates.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_coordinates.dart new file mode 100644 index 000000000000..185a97e08f00 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_coordinates.dart @@ -0,0 +1,100 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +const CameraPosition _kInitialPosition = + CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); + +class MapCoordinatesPage extends GoogleMapExampleAppPage { + const MapCoordinatesPage({Key? key}) + : super(const Icon(Icons.map), 'Map coordinates', key: key); + + @override + Widget build(BuildContext context) { + return const _MapCoordinatesBody(); + } +} + +class _MapCoordinatesBody extends StatefulWidget { + const _MapCoordinatesBody(); + + @override + State createState() => _MapCoordinatesBodyState(); +} + +class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { + _MapCoordinatesBodyState(); + + ExampleGoogleMapController? mapController; + LatLngBounds _visibleRegion = LatLngBounds( + southwest: const LatLng(0, 0), + northeast: const LatLng(0, 0), + ); + + @override + Widget build(BuildContext context) { + final ExampleGoogleMap googleMap = ExampleGoogleMap( + onMapCreated: onMapCreated, + initialCameraPosition: _kInitialPosition, + onCameraIdle: + _updateVisibleRegion, // https://github.com/flutter/flutter/issues/54758 + ); + + return NotificationListener( + onNotification: (ScrollNotification scrollState) { + _updateVisibleRegion(); + return true; + }, + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: googleMap, + ), + ), + ), + if (mapController != null) + Center( + child: Text('VisibleRegion:' + '\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 + Container( + width: 300, + height: 1000, + ), + ], + ), + ); + } + + Future onMapCreated(ExampleGoogleMapController controller) async { + final LatLngBounds visibleRegion = await controller.getVisibleRegion(); + setState(() { + mapController = controller; + _visibleRegion = visibleRegion; + }); + } + + Future _updateVisibleRegion() async { + final LatLngBounds visibleRegion = await mapController!.getVisibleRegion(); + setState(() { + _visibleRegion = visibleRegion; + }); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_ui.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_ui.dart new file mode 100644 index 000000000000..009ee71d8400 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_ui.dart @@ -0,0 +1,357 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +final LatLngBounds sydneyBounds = LatLngBounds( + southwest: const LatLng(-34.022631, 150.620685), + northeast: const LatLng(-33.571835, 151.325952), +); + +class MapUiPage extends GoogleMapExampleAppPage { + const MapUiPage({Key? key}) + : super(const Icon(Icons.map), 'User interface', key: key); + + @override + Widget build(BuildContext context) { + return const MapUiBody(); + } +} + +class MapUiBody extends StatefulWidget { + const MapUiBody({Key? key}) : super(key: key); + + @override + State createState() => MapUiBodyState(); +} + +class MapUiBodyState extends State { + MapUiBodyState(); + + static const CameraPosition _kInitialPosition = CameraPosition( + target: LatLng(-33.852, 151.211), + zoom: 11.0, + ); + + CameraPosition _position = _kInitialPosition; + bool _isMapCreated = false; + final bool _isMoving = false; + bool _compassEnabled = true; + bool _mapToolbarEnabled = true; + CameraTargetBounds _cameraTargetBounds = CameraTargetBounds.unbounded; + MinMaxZoomPreference _minMaxZoomPreference = MinMaxZoomPreference.unbounded; + MapType _mapType = MapType.normal; + bool _rotateGesturesEnabled = true; + bool _scrollGesturesEnabled = true; + bool _tiltGesturesEnabled = true; + bool _zoomControlsEnabled = false; + bool _zoomGesturesEnabled = true; + bool _indoorViewEnabled = true; + bool _myLocationEnabled = true; + bool _myTrafficEnabled = false; + bool _myLocationButtonEnabled = true; + late ExampleGoogleMapController _controller; + bool _nightMode = false; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + Widget _compassToggler() { + return TextButton( + child: Text('${_compassEnabled ? 'disable' : 'enable'} compass'), + onPressed: () { + setState(() { + _compassEnabled = !_compassEnabled; + }); + }, + ); + } + + Widget _mapToolbarToggler() { + return TextButton( + child: Text('${_mapToolbarEnabled ? 'disable' : 'enable'} map toolbar'), + onPressed: () { + setState(() { + _mapToolbarEnabled = !_mapToolbarEnabled; + }); + }, + ); + } + + Widget _latLngBoundsToggler() { + return TextButton( + child: Text( + _cameraTargetBounds.bounds == null + ? 'bound camera target' + : 'release camera target', + ), + onPressed: () { + setState(() { + _cameraTargetBounds = _cameraTargetBounds.bounds == null + ? CameraTargetBounds(sydneyBounds) + : CameraTargetBounds.unbounded; + }); + }, + ); + } + + Widget _zoomBoundsToggler() { + return TextButton( + child: Text(_minMaxZoomPreference.minZoom == null + ? 'bound zoom' + : 'release zoom'), + onPressed: () { + setState(() { + _minMaxZoomPreference = _minMaxZoomPreference.minZoom == null + ? const MinMaxZoomPreference(12.0, 16.0) + : MinMaxZoomPreference.unbounded; + }); + }, + ); + } + + Widget _mapTypeCycler() { + final MapType nextType = + MapType.values[(_mapType.index + 1) % MapType.values.length]; + return TextButton( + child: Text('change map type to $nextType'), + onPressed: () { + setState(() { + _mapType = nextType; + }); + }, + ); + } + + Widget _rotateToggler() { + return TextButton( + child: Text('${_rotateGesturesEnabled ? 'disable' : 'enable'} rotate'), + onPressed: () { + setState(() { + _rotateGesturesEnabled = !_rotateGesturesEnabled; + }); + }, + ); + } + + Widget _scrollToggler() { + return TextButton( + child: Text('${_scrollGesturesEnabled ? 'disable' : 'enable'} scroll'), + onPressed: () { + setState(() { + _scrollGesturesEnabled = !_scrollGesturesEnabled; + }); + }, + ); + } + + Widget _tiltToggler() { + return TextButton( + child: Text('${_tiltGesturesEnabled ? 'disable' : 'enable'} tilt'), + onPressed: () { + setState(() { + _tiltGesturesEnabled = !_tiltGesturesEnabled; + }); + }, + ); + } + + Widget _zoomToggler() { + return TextButton( + child: Text('${_zoomGesturesEnabled ? 'disable' : 'enable'} zoom'), + onPressed: () { + setState(() { + _zoomGesturesEnabled = !_zoomGesturesEnabled; + }); + }, + ); + } + + Widget _zoomControlsToggler() { + return TextButton( + child: + Text('${_zoomControlsEnabled ? 'disable' : 'enable'} zoom controls'), + onPressed: () { + setState(() { + _zoomControlsEnabled = !_zoomControlsEnabled; + }); + }, + ); + } + + Widget _indoorViewToggler() { + return TextButton( + child: Text('${_indoorViewEnabled ? 'disable' : 'enable'} indoor'), + onPressed: () { + setState(() { + _indoorViewEnabled = !_indoorViewEnabled; + }); + }, + ); + } + + Widget _myLocationToggler() { + return TextButton( + child: Text( + '${_myLocationEnabled ? 'disable' : 'enable'} my location marker'), + onPressed: () { + setState(() { + _myLocationEnabled = !_myLocationEnabled; + }); + }, + ); + } + + Widget _myLocationButtonToggler() { + return TextButton( + child: Text( + '${_myLocationButtonEnabled ? 'disable' : 'enable'} my location button'), + onPressed: () { + setState(() { + _myLocationButtonEnabled = !_myLocationButtonEnabled; + }); + }, + ); + } + + Widget _myTrafficToggler() { + return TextButton( + child: Text('${_myTrafficEnabled ? 'disable' : 'enable'} my traffic'), + onPressed: () { + setState(() { + _myTrafficEnabled = !_myTrafficEnabled; + }); + }, + ); + } + + Future _getFileData(String path) async { + return await rootBundle.loadString(path); + } + + void _setMapStyle(String mapStyle) { + setState(() { + _nightMode = true; + _controller.setMapStyle(mapStyle); + }); + } + + // Should only be called if _isMapCreated is true. + Widget _nightModeToggler() { + assert(_isMapCreated); + return TextButton( + child: Text('${_nightMode ? 'disable' : 'enable'} night mode'), + onPressed: () { + if (_nightMode) { + setState(() { + _nightMode = false; + _controller.setMapStyle(null); + }); + } else { + _getFileData('assets/night_mode.json').then(_setMapStyle); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + final ExampleGoogleMap googleMap = ExampleGoogleMap( + onMapCreated: onMapCreated, + initialCameraPosition: _kInitialPosition, + compassEnabled: _compassEnabled, + mapToolbarEnabled: _mapToolbarEnabled, + cameraTargetBounds: _cameraTargetBounds, + minMaxZoomPreference: _minMaxZoomPreference, + mapType: _mapType, + rotateGesturesEnabled: _rotateGesturesEnabled, + scrollGesturesEnabled: _scrollGesturesEnabled, + tiltGesturesEnabled: _tiltGesturesEnabled, + zoomGesturesEnabled: _zoomGesturesEnabled, + zoomControlsEnabled: _zoomControlsEnabled, + indoorViewEnabled: _indoorViewEnabled, + myLocationEnabled: _myLocationEnabled, + myLocationButtonEnabled: _myLocationButtonEnabled, + trafficEnabled: _myTrafficEnabled, + onCameraMove: _updateCameraPosition, + ); + + final List columnChildren = [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: googleMap, + ), + ), + ), + ]; + + if (_isMapCreated) { + columnChildren.add( + Expanded( + child: ListView( + children: [ + Text('camera bearing: ${_position.bearing}'), + Text( + 'camera target: ${_position.target.latitude.toStringAsFixed(4)},' + '${_position.target.longitude.toStringAsFixed(4)}'), + Text('camera zoom: ${_position.zoom}'), + Text('camera tilt: ${_position.tilt}'), + Text(_isMoving ? '(Camera moving)' : '(Camera idle)'), + _compassToggler(), + _mapToolbarToggler(), + _latLngBoundsToggler(), + _mapTypeCycler(), + _zoomBoundsToggler(), + _rotateToggler(), + _scrollToggler(), + _tiltToggler(), + _zoomToggler(), + _zoomControlsToggler(), + _indoorViewToggler(), + _myLocationToggler(), + _myLocationButtonToggler(), + _myTrafficToggler(), + _nightModeToggler(), + ], + ), + ), + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: columnChildren, + ); + } + + void _updateCameraPosition(CameraPosition position) { + setState(() { + _position = position; + }); + } + + void onMapCreated(ExampleGoogleMapController controller) { + setState(() { + _controller = controller; + _isMapCreated = true; + }); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/marker_icons.dart new file mode 100644 index 000000000000..fe28eb680596 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/marker_icons.dart @@ -0,0 +1,98 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs +// ignore_for_file: unawaited_futures + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class MarkerIconsPage extends GoogleMapExampleAppPage { + const MarkerIconsPage({Key? key}) + : super(const Icon(Icons.image), 'Marker icons', key: key); + + @override + Widget build(BuildContext context) { + return const MarkerIconsBody(); + } +} + +class MarkerIconsBody extends StatefulWidget { + const MarkerIconsBody({Key? key}) : super(key: key); + + @override + State createState() => MarkerIconsBodyState(); +} + +const LatLng _kMapCenter = LatLng(52.4478, -3.5402); + +class MarkerIconsBodyState extends State { + ExampleGoogleMapController? controller; + BitmapDescriptor? _markerIcon; + + @override + Widget build(BuildContext context) { + _createMarkerImageFromAsset(context); + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: _kMapCenter, + zoom: 7.0, + ), + markers: {_createMarker()}, + onMapCreated: _onMapCreated, + ), + ), + ) + ], + ); + } + + Marker _createMarker() { + if (_markerIcon != null) { + return Marker( + markerId: const MarkerId('marker_1'), + position: _kMapCenter, + icon: _markerIcon!, + ); + } else { + return const Marker( + markerId: MarkerId('marker_1'), + position: _kMapCenter, + ); + } + } + + Future _createMarkerImageFromAsset(BuildContext context) async { + if (_markerIcon == null) { + final ImageConfiguration imageConfiguration = + createLocalImageConfiguration(context, size: const Size.square(48)); + BitmapDescriptor.fromAssetImage( + imageConfiguration, 'assets/red_square.png') + .then(_updateBitmap); + } + } + + void _updateBitmap(BitmapDescriptor bitmap) { + setState(() { + _markerIcon = bitmap; + }); + } + + void _onMapCreated(ExampleGoogleMapController controllerParam) { + setState(() { + controller = controllerParam; + }); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/move_camera.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/move_camera.dart new file mode 100644 index 000000000000..7f44d89518dc --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/move_camera.dart @@ -0,0 +1,171 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class MoveCameraPage extends GoogleMapExampleAppPage { + const MoveCameraPage({Key? key}) + : super(const Icon(Icons.map), 'Camera control', key: key); + + @override + Widget build(BuildContext context) { + return const MoveCamera(); + } +} + +class MoveCamera extends StatefulWidget { + const MoveCamera({Key? key}) : super(key: key); + @override + State createState() => MoveCameraState(); +} + +class MoveCameraState extends State { + ExampleGoogleMapController? mapController; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + mapController = controller; + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: ExampleGoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: + const CameraPosition(target: LatLng(0.0, 0.0)), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.newCameraPosition( + const CameraPosition( + bearing: 270.0, + target: LatLng(51.5160895, -0.1294527), + tilt: 30.0, + zoom: 17.0, + ), + ), + ); + }, + child: const Text('newCameraPosition'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.newLatLng( + const LatLng(56.1725505, 10.1850512), + ), + ); + }, + child: const Text('newLatLng'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.newLatLngBounds( + LatLngBounds( + southwest: const LatLng(-38.483935, 113.248673), + northeast: const LatLng(-8.982446, 153.823821), + ), + 10.0, + ), + ); + }, + child: const Text('newLatLngBounds'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.newLatLngZoom( + const LatLng(37.4231613, -122.087159), + 11.0, + ), + ); + }, + child: const Text('newLatLngZoom'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.scrollBy(150.0, -225.0), + ); + }, + child: const Text('scrollBy'), + ), + ], + ), + Column( + children: [ + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.zoomBy( + -0.5, + const Offset(30.0, 20.0), + ), + ); + }, + child: const Text('zoomBy with focus'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.zoomBy(-0.5), + ); + }, + child: const Text('zoomBy'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.zoomIn(), + ); + }, + child: const Text('zoomIn'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.zoomOut(), + ); + }, + child: const Text('zoomOut'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.zoomTo(16.0), + ); + }, + child: const Text('zoomTo'), + ), + ], + ), + ], + ) + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/padding.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/padding.dart new file mode 100644 index 000000000000..98be700a2af2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/padding.dart @@ -0,0 +1,180 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class PaddingPage extends GoogleMapExampleAppPage { + const PaddingPage({Key? key}) + : super(const Icon(Icons.map), 'Add padding to the map', key: key); + + @override + Widget build(BuildContext context) { + return const MarkerIconsBody(); + } +} + +class MarkerIconsBody extends StatefulWidget { + const MarkerIconsBody({Key? key}) : super(key: key); + + @override + State createState() => MarkerIconsBodyState(); +} + +const LatLng _kMapCenter = LatLng(52.4478, -3.5402); + +class MarkerIconsBodyState extends State { + ExampleGoogleMapController? controller; + + EdgeInsets _padding = EdgeInsets.zero; + + @override + Widget build(BuildContext context) { + final ExampleGoogleMap googleMap = ExampleGoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition( + target: _kMapCenter, + zoom: 7.0, + ), + padding: _padding, + ); + + final List columnChildren = [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: googleMap, + ), + ), + ), + const Padding( + padding: EdgeInsets.only(top: 20), + child: Center( + child: Text( + 'Enter Padding Below', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + ), + ]; + + columnChildren.addAll([_paddingInput(), _buttons()]); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: columnChildren, + ); + } + + void _onMapCreated(ExampleGoogleMapController controllerParam) { + setState(() { + controller = controllerParam; + }); + } + + final TextEditingController _topController = TextEditingController(); + final TextEditingController _bottomController = TextEditingController(); + final TextEditingController _leftController = TextEditingController(); + final TextEditingController _rightController = TextEditingController(); + + Widget _paddingInput() { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Flexible( + flex: 2, + child: TextField( + controller: _topController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + decoration: const InputDecoration( + hintText: 'Top', + ), + ), + ), + const Spacer(), + Flexible( + flex: 2, + child: TextField( + controller: _bottomController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + decoration: const InputDecoration( + hintText: 'Bottom', + ), + ), + ), + const Spacer(), + Flexible( + flex: 2, + child: TextField( + controller: _leftController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + decoration: const InputDecoration( + hintText: 'Left', + ), + ), + ), + const Spacer(), + Flexible( + flex: 2, + child: TextField( + controller: _rightController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + decoration: const InputDecoration( + hintText: 'Right', + ), + ), + ), + ], + ), + ); + } + + Widget _buttons() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + child: const Text('Set Padding'), + onPressed: () { + setState(() { + _padding = EdgeInsets.fromLTRB( + double.tryParse(_leftController.value.text) ?? 0, + double.tryParse(_topController.value.text) ?? 0, + double.tryParse(_rightController.value.text) ?? 0, + double.tryParse(_bottomController.value.text) ?? 0); + }); + }, + ), + TextButton( + child: const Text('Reset Padding'), + onPressed: () { + setState(() { + _topController.clear(); + _bottomController.clear(); + _leftController.clear(); + _rightController.clear(); + _padding = EdgeInsets.zero; + }); + }, + ) + ], + ), + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/page.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/page.dart new file mode 100644 index 000000000000..eb01ab07a6f3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/page.dart @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; + +abstract class GoogleMapExampleAppPage extends StatelessWidget { + const GoogleMapExampleAppPage(this.leading, this.title, {Key? key}) + : super(key: key); + + final Widget leading; + final String title; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_circle.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_circle.dart new file mode 100644 index 000000000000..9dc5760afa1f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_circle.dart @@ -0,0 +1,232 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class PlaceCirclePage extends GoogleMapExampleAppPage { + const PlaceCirclePage({Key? key}) + : super(const Icon(Icons.linear_scale), 'Place circle', key: key); + + @override + Widget build(BuildContext context) { + return const PlaceCircleBody(); + } +} + +class PlaceCircleBody extends StatefulWidget { + const PlaceCircleBody({Key? key}) : super(key: key); + + @override + State createState() => PlaceCircleBodyState(); +} + +class PlaceCircleBodyState extends State { + PlaceCircleBodyState(); + + ExampleGoogleMapController? controller; + Map circles = {}; + int _circleIdCounter = 1; + CircleId? selectedCircle; + + // Values when toggling circle color + int fillColorsIndex = 0; + int strokeColorsIndex = 0; + List colors = [ + Colors.purple, + Colors.red, + Colors.green, + Colors.pink, + ]; + + // Values when toggling circle stroke width + int widthsIndex = 0; + List widths = [10, 20, 5]; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _onCircleTapped(CircleId circleId) { + setState(() { + selectedCircle = circleId; + }); + } + + void _remove(CircleId circleId) { + setState(() { + if (circles.containsKey(circleId)) { + circles.remove(circleId); + } + if (circleId == selectedCircle) { + selectedCircle = null; + } + }); + } + + void _add() { + final int circleCount = circles.length; + + if (circleCount == 12) { + return; + } + + final String circleIdVal = 'circle_id_$_circleIdCounter'; + _circleIdCounter++; + final CircleId circleId = CircleId(circleIdVal); + + final Circle circle = Circle( + circleId: circleId, + consumeTapEvents: true, + strokeColor: Colors.orange, + fillColor: Colors.green, + strokeWidth: 5, + center: _createCenter(), + radius: 50000, + onTap: () { + _onCircleTapped(circleId); + }, + ); + + setState(() { + circles[circleId] = circle; + }); + } + + void _toggleVisible(CircleId circleId) { + final Circle circle = circles[circleId]!; + setState(() { + circles[circleId] = circle.copyWith( + visibleParam: !circle.visible, + ); + }); + } + + void _changeFillColor(CircleId circleId) { + final Circle circle = circles[circleId]!; + setState(() { + circles[circleId] = circle.copyWith( + fillColorParam: colors[++fillColorsIndex % colors.length], + ); + }); + } + + void _changeStrokeColor(CircleId circleId) { + final Circle circle = circles[circleId]!; + setState(() { + circles[circleId] = circle.copyWith( + strokeColorParam: colors[++strokeColorsIndex % colors.length], + ); + }); + } + + void _changeStrokeWidth(CircleId circleId) { + final Circle circle = circles[circleId]!; + setState(() { + circles[circleId] = circle.copyWith( + strokeWidthParam: widths[++widthsIndex % widths.length], + ); + }); + } + + @override + Widget build(BuildContext context) { + final CircleId? selectedId = selectedCircle; + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: LatLng(52.4478, -3.5402), + zoom: 7.0, + ), + circles: Set.of(circles.values), + onMapCreated: _onMapCreated, + ), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Column( + children: [ + TextButton( + onPressed: _add, + child: const Text('add'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _remove(selectedId), + child: const Text('remove'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _toggleVisible(selectedId), + child: const Text('toggle visible'), + ), + ], + ), + Column( + children: [ + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeStrokeWidth(selectedId), + child: const Text('change stroke width'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeStrokeColor(selectedId), + child: const Text('change stroke color'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeFillColor(selectedId), + child: const Text('change fill color'), + ), + ], + ) + ], + ) + ], + ), + ), + ), + ], + ); + } + + LatLng _createCenter() { + final double offset = _circleIdCounter.ceilToDouble(); + return _createLatLng(51.4816 + offset * 0.2, -3.1791); + } + + LatLng _createLatLng(double lat, double lng) { + return LatLng(lat, lng); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart new file mode 100644 index 000000000000..7d12f4c81684 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart @@ -0,0 +1,421 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:math'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class PlaceMarkerPage extends GoogleMapExampleAppPage { + const PlaceMarkerPage({Key? key}) + : super(const Icon(Icons.place), 'Place marker', key: key); + + @override + Widget build(BuildContext context) { + return const PlaceMarkerBody(); + } +} + +class PlaceMarkerBody extends StatefulWidget { + const PlaceMarkerBody({Key? key}) : super(key: key); + + @override + State createState() => PlaceMarkerBodyState(); +} + +typedef MarkerUpdateAction = Marker Function(Marker marker); + +class PlaceMarkerBodyState extends State { + PlaceMarkerBodyState(); + static const LatLng center = LatLng(-33.86711, 151.1947171); + + ExampleGoogleMapController? controller; + Map markers = {}; + MarkerId? selectedMarker; + int _markerIdCounter = 1; + LatLng? markerPosition; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _onMarkerTapped(MarkerId markerId) { + final Marker? tappedMarker = markers[markerId]; + if (tappedMarker != null) { + setState(() { + final MarkerId? previousMarkerId = selectedMarker; + if (previousMarkerId != null && markers.containsKey(previousMarkerId)) { + final Marker resetOld = markers[previousMarkerId]! + .copyWith(iconParam: BitmapDescriptor.defaultMarker); + markers[previousMarkerId] = resetOld; + } + selectedMarker = markerId; + final Marker newMarker = tappedMarker.copyWith( + iconParam: BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueGreen, + ), + ); + markers[markerId] = newMarker; + + markerPosition = null; + }); + } + } + + Future _onMarkerDrag(MarkerId markerId, LatLng newPosition) async { + setState(() { + markerPosition = newPosition; + }); + } + + Future _onMarkerDragEnd(MarkerId markerId, LatLng newPosition) async { + final Marker? tappedMarker = markers[markerId]; + if (tappedMarker != null) { + setState(() { + markerPosition = null; + }); + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () => Navigator.of(context).pop(), + ) + ], + content: Padding( + padding: const EdgeInsets.symmetric(vertical: 66), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Old position: ${tappedMarker.position}'), + Text('New position: $newPosition'), + ], + ))); + }); + } + } + + void _add() { + final int markerCount = markers.length; + + if (markerCount == 12) { + return; + } + + final String markerIdVal = 'marker_id_$_markerIdCounter'; + _markerIdCounter++; + final MarkerId markerId = MarkerId(markerIdVal); + + final Marker marker = Marker( + markerId: markerId, + position: LatLng( + center.latitude + sin(_markerIdCounter * pi / 6.0) / 20.0, + center.longitude + cos(_markerIdCounter * pi / 6.0) / 20.0, + ), + infoWindow: InfoWindow(title: markerIdVal, snippet: '*'), + onTap: () => _onMarkerTapped(markerId), + onDragEnd: (LatLng position) => _onMarkerDragEnd(markerId, position), + onDrag: (LatLng position) => _onMarkerDrag(markerId, position), + ); + + setState(() { + markers[markerId] = marker; + }); + } + + void _remove(MarkerId markerId) { + setState(() { + if (markers.containsKey(markerId)) { + markers.remove(markerId); + } + }); + } + + void _changePosition(MarkerId markerId) { + final Marker marker = markers[markerId]!; + final LatLng current = marker.position; + final Offset offset = Offset( + center.latitude - current.latitude, + center.longitude - current.longitude, + ); + setState(() { + markers[markerId] = marker.copyWith( + positionParam: LatLng( + center.latitude + offset.dy, + center.longitude + offset.dx, + ), + ); + }); + } + + void _changeAnchor(MarkerId markerId) { + final Marker marker = markers[markerId]!; + final Offset currentAnchor = marker.anchor; + final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx); + setState(() { + markers[markerId] = marker.copyWith( + anchorParam: newAnchor, + ); + }); + } + + Future _changeInfoAnchor(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + final Offset currentAnchor = marker.infoWindow.anchor; + final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx); + setState(() { + markers[markerId] = marker.copyWith( + infoWindowParam: marker.infoWindow.copyWith( + anchorParam: newAnchor, + ), + ); + }); + } + + Future _toggleDraggable(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith( + draggableParam: !marker.draggable, + ); + }); + } + + Future _toggleFlat(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith( + flatParam: !marker.flat, + ); + }); + } + + Future _changeInfo(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + final String newSnippet = '${marker.infoWindow.snippet!}*'; + setState(() { + markers[markerId] = marker.copyWith( + infoWindowParam: marker.infoWindow.copyWith( + snippetParam: newSnippet, + ), + ); + }); + } + + Future _changeAlpha(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + final double current = marker.alpha; + setState(() { + markers[markerId] = marker.copyWith( + alphaParam: current < 0.1 ? 1.0 : current * 0.75, + ); + }); + } + + Future _changeRotation(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + final double current = marker.rotation; + setState(() { + markers[markerId] = marker.copyWith( + rotationParam: current == 330.0 ? 0.0 : current + 30.0, + ); + }); + } + + Future _toggleVisible(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith( + visibleParam: !marker.visible, + ); + }); + } + + Future _changeZIndex(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + final double current = marker.zIndex; + setState(() { + markers[markerId] = marker.copyWith( + zIndexParam: current == 12.0 ? 0.0 : current + 1.0, + ); + }); + } + + void _setMarkerIcon(MarkerId markerId, BitmapDescriptor assetIcon) { + final Marker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith( + iconParam: assetIcon, + ); + }); + } + + Future _getAssetIcon(BuildContext context) async { + final Completer bitmapIcon = + Completer(); + final ImageConfiguration config = createLocalImageConfiguration(context); + + const AssetImage('assets/red_square.png') + .resolve(config) + .addListener(ImageStreamListener((ImageInfo image, bool sync) async { + final ByteData? bytes = + await image.image.toByteData(format: ImageByteFormat.png); + if (bytes == null) { + bitmapIcon.completeError(Exception('Unable to encode icon')); + return; + } + final BitmapDescriptor bitmap = + BitmapDescriptor.fromBytes(bytes.buffer.asUint8List()); + bitmapIcon.complete(bitmap); + })); + + return await bitmapIcon.future; + } + + @override + Widget build(BuildContext context) { + final MarkerId? selectedId = selectedMarker; + return Stack(children: [ + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ExampleGoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition( + target: LatLng(-33.852, 151.211), + zoom: 11.0, + ), + markers: Set.of(markers.values), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: _add, + child: const Text('Add'), + ), + TextButton( + onPressed: + selectedId == null ? null : () => _remove(selectedId), + child: const Text('Remove'), + ), + ], + ), + Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: + selectedId == null ? null : () => _changeInfo(selectedId), + child: const Text('change info'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _changeInfoAnchor(selectedId), + child: const Text('change info anchor'), + ), + TextButton( + onPressed: + selectedId == null ? null : () => _changeAlpha(selectedId), + child: const Text('change alpha'), + ), + TextButton( + onPressed: + selectedId == null ? null : () => _changeAnchor(selectedId), + child: const Text('change anchor'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _toggleDraggable(selectedId), + child: const Text('toggle draggable'), + ), + TextButton( + onPressed: + selectedId == null ? null : () => _toggleFlat(selectedId), + child: const Text('toggle flat'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _changePosition(selectedId), + child: const Text('change position'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _changeRotation(selectedId), + child: const Text('change rotation'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _toggleVisible(selectedId), + child: const Text('toggle visible'), + ), + TextButton( + onPressed: + selectedId == null ? null : () => _changeZIndex(selectedId), + child: const Text('change zIndex'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () { + _getAssetIcon(context).then( + (BitmapDescriptor icon) { + _setMarkerIcon(selectedId, icon); + }, + ); + }, + child: const Text('set marker icon'), + ), + ], + ), + ], + ), + Visibility( + visible: markerPosition != null, + child: Container( + color: Colors.white70, + height: 30, + padding: const EdgeInsets.only(left: 12, right: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + if (markerPosition == null) + Container() + else + Expanded(child: Text('lat: ${markerPosition!.latitude}')), + if (markerPosition == null) + Container() + else + Expanded(child: Text('lng: ${markerPosition!.longitude}')), + ], + ), + ), + ), + ]); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_polygon.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_polygon.dart new file mode 100644 index 000000000000..b41cb5d3ccb1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_polygon.dart @@ -0,0 +1,306 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class PlacePolygonPage extends GoogleMapExampleAppPage { + const PlacePolygonPage({Key? key}) + : super(const Icon(Icons.linear_scale), 'Place polygon', key: key); + + @override + Widget build(BuildContext context) { + return const PlacePolygonBody(); + } +} + +class PlacePolygonBody extends StatefulWidget { + const PlacePolygonBody({Key? key}) : super(key: key); + + @override + State createState() => PlacePolygonBodyState(); +} + +class PlacePolygonBodyState extends State { + PlacePolygonBodyState(); + + ExampleGoogleMapController? controller; + Map polygons = {}; + Map polygonOffsets = {}; + int _polygonIdCounter = 0; + PolygonId? selectedPolygon; + + // Values when toggling polygon color + int strokeColorsIndex = 0; + int fillColorsIndex = 0; + List colors = [ + Colors.purple, + Colors.red, + Colors.green, + Colors.pink, + ]; + + // Values when toggling polygon width + int widthsIndex = 0; + List widths = [10, 20, 5]; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _onPolygonTapped(PolygonId polygonId) { + setState(() { + selectedPolygon = polygonId; + }); + } + + void _remove(PolygonId polygonId) { + setState(() { + if (polygons.containsKey(polygonId)) { + polygons.remove(polygonId); + } + selectedPolygon = null; + }); + } + + void _add() { + final int polygonCount = polygons.length; + + if (polygonCount == 12) { + return; + } + + final String polygonIdVal = 'polygon_id_$_polygonIdCounter'; + final PolygonId polygonId = PolygonId(polygonIdVal); + + final Polygon polygon = Polygon( + polygonId: polygonId, + consumeTapEvents: true, + strokeColor: Colors.orange, + strokeWidth: 5, + fillColor: Colors.green, + points: _createPoints(), + onTap: () { + _onPolygonTapped(polygonId); + }, + ); + + setState(() { + polygons[polygonId] = polygon; + polygonOffsets[polygonId] = _polygonIdCounter.ceilToDouble(); + // increment _polygonIdCounter to have unique polygon id each time + _polygonIdCounter++; + }); + } + + void _toggleGeodesic(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + geodesicParam: !polygon.geodesic, + ); + }); + } + + void _toggleVisible(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + visibleParam: !polygon.visible, + ); + }); + } + + void _changeStrokeColor(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + strokeColorParam: colors[++strokeColorsIndex % colors.length], + ); + }); + } + + void _changeFillColor(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + fillColorParam: colors[++fillColorsIndex % colors.length], + ); + }); + } + + void _changeWidth(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + strokeWidthParam: widths[++widthsIndex % widths.length], + ); + }); + } + + void _addHoles(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = + polygon.copyWith(holesParam: _createHoles(polygonId)); + }); + } + + void _removeHoles(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + holesParam: >[], + ); + }); + } + + @override + Widget build(BuildContext context) { + final PolygonId? selectedId = selectedPolygon; + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: LatLng(52.4478, -3.5402), + zoom: 7.0, + ), + polygons: Set.of(polygons.values), + onMapCreated: _onMapCreated, + ), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Column( + children: [ + TextButton( + onPressed: _add, + child: const Text('add'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _remove(selectedId), + child: const Text('remove'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _toggleVisible(selectedId), + child: const Text('toggle visible'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _toggleGeodesic(selectedId), + child: const Text('toggle geodesic'), + ), + ], + ), + Column( + children: [ + TextButton( + onPressed: (selectedId == null) + ? null + : ((polygons[selectedId]!.holes.isNotEmpty) + ? null + : () => _addHoles(selectedId)), + child: const Text('add holes'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : ((polygons[selectedId]!.holes.isEmpty) + ? null + : () => _removeHoles(selectedId)), + child: const Text('remove holes'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeWidth(selectedId), + child: const Text('change stroke width'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeStrokeColor(selectedId), + child: const Text('change stroke color'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeFillColor(selectedId), + child: const Text('change fill color'), + ), + ], + ) + ], + ) + ], + ), + ), + ), + ], + ); + } + + List _createPoints() { + final List points = []; + final double offset = _polygonIdCounter.ceilToDouble(); + points.add(_createLatLng(51.2395 + offset, -3.4314)); + points.add(_createLatLng(53.5234 + offset, -3.5314)); + points.add(_createLatLng(52.4351 + offset, -4.5235)); + points.add(_createLatLng(52.1231 + offset, -5.0829)); + return points; + } + + List> _createHoles(PolygonId polygonId) { + final List> holes = >[]; + final double offset = polygonOffsets[polygonId]!; + + final List hole1 = []; + hole1.add(_createLatLng(51.8395 + offset, -3.8814)); + hole1.add(_createLatLng(52.0234 + offset, -3.9914)); + hole1.add(_createLatLng(52.1351 + offset, -4.4435)); + hole1.add(_createLatLng(52.0231 + offset, -4.5829)); + holes.add(hole1); + + final List hole2 = []; + hole2.add(_createLatLng(52.2395 + offset, -3.6814)); + hole2.add(_createLatLng(52.4234 + offset, -3.7914)); + hole2.add(_createLatLng(52.5351 + offset, -4.2435)); + hole2.add(_createLatLng(52.4231 + offset, -4.3829)); + holes.add(hole2); + + return holes; + } + + LatLng _createLatLng(double lat, double lng) { + return LatLng(lat, lng); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_polyline.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_polyline.dart new file mode 100644 index 000000000000..004206b9f6cc --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_polyline.dart @@ -0,0 +1,325 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class PlacePolylinePage extends GoogleMapExampleAppPage { + const PlacePolylinePage({Key? key}) + : super(const Icon(Icons.linear_scale), 'Place polyline', key: key); + + @override + Widget build(BuildContext context) { + return const PlacePolylineBody(); + } +} + +class PlacePolylineBody extends StatefulWidget { + const PlacePolylineBody({Key? key}) : super(key: key); + + @override + State createState() => PlacePolylineBodyState(); +} + +class PlacePolylineBodyState extends State { + PlacePolylineBodyState(); + + ExampleGoogleMapController? controller; + Map polylines = {}; + int _polylineIdCounter = 0; + PolylineId? selectedPolyline; + + // Values when toggling polyline color + int colorsIndex = 0; + List colors = [ + Colors.purple, + Colors.red, + Colors.green, + Colors.pink, + ]; + + // Values when toggling polyline width + int widthsIndex = 0; + List widths = [10, 20, 5]; + + int jointTypesIndex = 0; + List jointTypes = [ + JointType.mitered, + JointType.bevel, + JointType.round + ]; + + // Values when toggling polyline end cap type + int endCapsIndex = 0; + List endCaps = [Cap.buttCap, Cap.squareCap, Cap.roundCap]; + + // Values when toggling polyline start cap type + int startCapsIndex = 0; + List startCaps = [Cap.buttCap, Cap.squareCap, Cap.roundCap]; + + // Values when toggling polyline pattern + int patternsIndex = 0; + List> patterns = >[ + [], + [ + PatternItem.dash(30.0), + PatternItem.gap(20.0), + PatternItem.dot, + PatternItem.gap(20.0) + ], + [PatternItem.dash(30.0), PatternItem.gap(20.0)], + [PatternItem.dot, PatternItem.gap(10.0)], + ]; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _onPolylineTapped(PolylineId polylineId) { + setState(() { + selectedPolyline = polylineId; + }); + } + + void _remove(PolylineId polylineId) { + setState(() { + if (polylines.containsKey(polylineId)) { + polylines.remove(polylineId); + } + selectedPolyline = null; + }); + } + + void _add() { + final int polylineCount = polylines.length; + + if (polylineCount == 12) { + return; + } + + final String polylineIdVal = 'polyline_id_$_polylineIdCounter'; + _polylineIdCounter++; + final PolylineId polylineId = PolylineId(polylineIdVal); + + final Polyline polyline = Polyline( + polylineId: polylineId, + consumeTapEvents: true, + color: Colors.orange, + width: 5, + points: _createPoints(), + onTap: () { + _onPolylineTapped(polylineId); + }, + ); + + setState(() { + polylines[polylineId] = polyline; + }); + } + + void _toggleGeodesic(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + geodesicParam: !polyline.geodesic, + ); + }); + } + + void _toggleVisible(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + visibleParam: !polyline.visible, + ); + }); + } + + void _changeColor(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + colorParam: colors[++colorsIndex % colors.length], + ); + }); + } + + void _changeWidth(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + widthParam: widths[++widthsIndex % widths.length], + ); + }); + } + + void _changeJointType(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + jointTypeParam: jointTypes[++jointTypesIndex % jointTypes.length], + ); + }); + } + + void _changeEndCap(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + endCapParam: endCaps[++endCapsIndex % endCaps.length], + ); + }); + } + + void _changeStartCap(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + startCapParam: startCaps[++startCapsIndex % startCaps.length], + ); + }); + } + + void _changePattern(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + patternsParam: patterns[++patternsIndex % patterns.length], + ); + }); + } + + @override + Widget build(BuildContext context) { + final bool isIOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; + + final PolylineId? selectedId = selectedPolyline; + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: LatLng(53.1721, -3.5402), + zoom: 7.0, + ), + polylines: Set.of(polylines.values), + onMapCreated: _onMapCreated, + ), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Column( + children: [ + TextButton( + onPressed: _add, + child: const Text('add'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _remove(selectedId), + child: const Text('remove'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _toggleVisible(selectedId), + child: const Text('toggle visible'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _toggleGeodesic(selectedId), + child: const Text('toggle geodesic'), + ), + ], + ), + Column( + children: [ + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeWidth(selectedId), + child: const Text('change width'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeColor(selectedId), + child: const Text('change color'), + ), + TextButton( + onPressed: isIOS || (selectedId == null) + ? null + : () => _changeStartCap(selectedId), + child: const Text('change start cap [Android only]'), + ), + TextButton( + onPressed: isIOS || (selectedId == null) + ? null + : () => _changeEndCap(selectedId), + child: const Text('change end cap [Android only]'), + ), + TextButton( + onPressed: isIOS || (selectedId == null) + ? null + : () => _changeJointType(selectedId), + child: const Text('change joint type [Android only]'), + ), + TextButton( + onPressed: isIOS || (selectedId == null) + ? null + : () => _changePattern(selectedId), + child: const Text('change pattern [Android only]'), + ), + ], + ) + ], + ) + ], + ), + ), + ), + ], + ); + } + + List _createPoints() { + final List points = []; + final double offset = _polylineIdCounter.ceilToDouble(); + points.add(_createLatLng(51.4816 + offset, -3.1791)); + points.add(_createLatLng(53.0430 + offset, -2.9925)); + points.add(_createLatLng(53.1396 + offset, -4.2739)); + points.add(_createLatLng(52.4153 + offset, -4.0829)); + return points; + } + + LatLng _createLatLng(double lat, double lng) { + return LatLng(lat, lng); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/readme_excerpts.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/readme_excerpts.dart new file mode 100644 index 000000000000..5911c062e444 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/readme_excerpts.dart @@ -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 'package:flutter/material.dart'; +// #docregion DisplayMode +import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + // Require Hybrid Composition mode on Android. + final GoogleMapsFlutterPlatform mapsImplementation = + GoogleMapsFlutterPlatform.instance; + if (mapsImplementation is GoogleMapsFlutterAndroid) { + mapsImplementation.useAndroidViewSurface = true; + } + // #enddocregion DisplayMode + runApp(const MaterialApp()); + // #docregion DisplayMode +} +// #enddocregion DisplayMode diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/scrolling_map.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/scrolling_map.dart new file mode 100644 index 000000000000..7a9b75cd1224 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/scrolling_map.dart @@ -0,0 +1,114 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +const LatLng _center = LatLng(32.080664, 34.9563837); + +class ScrollingMapPage extends GoogleMapExampleAppPage { + const ScrollingMapPage({Key? key}) + : super(const Icon(Icons.map), 'Scrolling map', key: key); + + @override + Widget build(BuildContext context) { + return const ScrollingMapBody(); + } +} + +class ScrollingMapBody extends StatelessWidget { + const ScrollingMapBody({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListView( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 30.0), + child: Column( + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 12.0), + child: Text('This map consumes all touch events.'), + ), + Center( + child: SizedBox( + width: 300.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: _center, + zoom: 11.0, + ), + gestureRecognizers: // + >{ + Factory( + () => EagerGestureRecognizer(), + ), + }, + ), + ), + ), + ], + ), + ), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 30.0), + child: Column( + children: [ + const Text("This map doesn't consume the vertical drags."), + const Padding( + padding: EdgeInsets.only(bottom: 12.0), + child: + Text('It still gets other gestures (e.g scale or tap).'), + ), + Center( + child: SizedBox( + width: 300.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: _center, + zoom: 11.0, + ), + markers: { + Marker( + markerId: const MarkerId('test_marker_id'), + position: LatLng( + _center.latitude, + _center.longitude, + ), + infoWindow: const InfoWindow( + title: 'An interesting location', + snippet: '*', + ), + ), + }, + gestureRecognizers: < + Factory>{ + Factory( + () => ScaleGestureRecognizer(), + ), + }, + ), + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/snapshot.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/snapshot.dart new file mode 100644 index 000000000000..56a90a8e49f6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/snapshot.dart @@ -0,0 +1,76 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +const CameraPosition _kInitialPosition = + CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); + +class SnapshotPage extends GoogleMapExampleAppPage { + const SnapshotPage({Key? key}) + : super(const Icon(Icons.camera_alt), 'Take a snapshot of the map', + key: key); + + @override + Widget build(BuildContext context) { + return _SnapshotBody(); + } +} + +class _SnapshotBody extends StatefulWidget { + @override + _SnapshotBodyState createState() => _SnapshotBodyState(); +} + +class _SnapshotBodyState extends State<_SnapshotBody> { + ExampleGoogleMapController? _mapController; + Uint8List? _imageBytes; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + height: 180, + child: ExampleGoogleMap( + onMapCreated: onMapCreated, + initialCameraPosition: _kInitialPosition, + ), + ), + TextButton( + child: const Text('Take a snapshot'), + onPressed: () async { + final Uint8List? imageBytes = + await _mapController?.takeSnapshot(); + setState(() { + _imageBytes = imageBytes; + }); + }, + ), + Container( + decoration: BoxDecoration(color: Colors.blueGrey[50]), + height: 180, + child: _imageBytes != null ? Image.memory(_imageBytes!) : null, + ), + ], + ), + ); + } + + // ignore: use_setters_to_change_properties + void onMapCreated(ExampleGoogleMapController controller) { + _mapController = controller; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/tile_overlay.dart new file mode 100644 index 000000000000..e25ab916d8de --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/tile_overlay.dart @@ -0,0 +1,154 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class TileOverlayPage extends GoogleMapExampleAppPage { + const TileOverlayPage({Key? key}) + : super(const Icon(Icons.map), 'Tile overlay', key: key); + + @override + Widget build(BuildContext context) { + return const TileOverlayBody(); + } +} + +class TileOverlayBody extends StatefulWidget { + const TileOverlayBody({Key? key}) : super(key: key); + + @override + State createState() => TileOverlayBodyState(); +} + +class TileOverlayBodyState extends State { + TileOverlayBodyState(); + + ExampleGoogleMapController? controller; + TileOverlay? _tileOverlay; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _removeTileOverlay() { + setState(() { + _tileOverlay = null; + }); + } + + void _addTileOverlay() { + final TileOverlay tileOverlay = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + ); + setState(() { + _tileOverlay = tileOverlay; + }); + } + + void _clearTileCache() { + if (_tileOverlay != null && controller != null) { + controller!.clearTileCache(_tileOverlay!.tileOverlayId); + } + } + + @override + Widget build(BuildContext context) { + final Set overlays = { + if (_tileOverlay != null) _tileOverlay!, + }; + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: LatLng(59.935460, 30.325177), + zoom: 7.0, + ), + tileOverlays: overlays, + onMapCreated: _onMapCreated, + ), + ), + ), + TextButton( + onPressed: _addTileOverlay, + child: const Text('Add tile overlay'), + ), + TextButton( + onPressed: _removeTileOverlay, + child: const Text('Remove tile overlay'), + ), + TextButton( + onPressed: _clearTileCache, + child: const Text('Clear tile cache'), + ), + ], + ); + } +} + +class _DebugTileProvider implements TileProvider { + _DebugTileProvider() { + boxPaint.isAntiAlias = true; + boxPaint.color = Colors.blue; + boxPaint.strokeWidth = 2.0; + boxPaint.style = PaintingStyle.stroke; + } + + static const int width = 100; + static const int height = 100; + static final Paint boxPaint = Paint(); + static const TextStyle textStyle = TextStyle( + color: Colors.red, + fontSize: 20, + ); + + @override + Future getTile(int x, int y, int? zoom) async { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final TextSpan textSpan = TextSpan( + text: '$x,$y', + style: textStyle, + ); + final TextPainter textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + ); + textPainter.layout( + maxWidth: width.toDouble(), + ); + textPainter.paint(canvas, Offset.zero); + canvas.drawRect( + Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint); + final ui.Picture picture = recorder.endRecording(); + final Uint8List byteData = await picture + .toImage(width, height) + .then((ui.Image image) => + image.toByteData(format: ui.ImageByteFormat.png)) + .then((ByteData? byteData) => byteData!.buffer.asUint8List()); + return Tile(width, height, byteData); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml new file mode 100644 index 000000000000..07decea39920 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml @@ -0,0 +1,36 @@ +name: google_maps_flutter_example +description: Demonstrates how to use the google_maps_flutter plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + cupertino_icons: ^0.1.0 + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.1 + google_maps_flutter_android: + # When depending on this package from a real application you should use: + # google_maps_flutter_android: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + google_maps_flutter_platform_interface: ^2.2.1 + +dev_dependencies: + build_runner: ^2.1.10 + espresso: ^0.2.0 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true + assets: + - assets/ diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/test_driver/integration_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/test_driver/integration_test.dart @@ -0,0 +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. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/google_maps_flutter_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/google_maps_flutter_android.dart new file mode 100644 index 000000000000..edd231efc691 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/google_maps_flutter_android.dart @@ -0,0 +1,5 @@ +// 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. + +export 'src/google_maps_flutter_android.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart new file mode 100644 index 000000000000..4e0cad78e869 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart @@ -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. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +/// An Android of implementation of [GoogleMapsInspectorPlatform]. +@visibleForTesting +class GoogleMapsInspectorAndroid extends GoogleMapsInspectorPlatform { + /// Creates a method-channel-based inspector instance that gets the channel + /// for a given map ID from [channelProvider]. + GoogleMapsInspectorAndroid(MethodChannel? Function(int mapId) channelProvider) + : _channelProvider = channelProvider; + + final MethodChannel? Function(int mapId) _channelProvider; + + @override + Future areBuildingsEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isBuildingsEnabled'))!; + } + + @override + Future areRotateGesturesEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isRotateGesturesEnabled'))!; + } + + @override + Future areScrollGesturesEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isScrollGesturesEnabled'))!; + } + + @override + Future areTiltGesturesEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isTiltGesturesEnabled'))!; + } + + @override + Future areZoomControlsEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isZoomControlsEnabled'))!; + } + + @override + Future areZoomGesturesEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isZoomGesturesEnabled'))!; + } + + @override + Future getMinMaxZoomLevels({required int mapId}) async { + final List zoomLevels = (await _channelProvider(mapId)! + .invokeMethod>('map#getMinMaxZoomLevels'))! + .cast(); + return MinMaxZoomPreference(zoomLevels[0], zoomLevels[1]); + } + + @override + Future getTileOverlayInfo(TileOverlayId tileOverlayId, + {required int mapId}) async { + final Map? tileInfo = await _channelProvider(mapId)! + .invokeMapMethod( + 'map#getTileOverlayInfo', { + 'tileOverlayId': tileOverlayId.value, + }); + if (tileInfo == null) { + return null; + } + return TileOverlay( + tileOverlayId: tileOverlayId, + fadeIn: tileInfo['fadeIn']! as bool, + transparency: tileInfo['transparency']! as double, + visible: tileInfo['visible']! as bool, + // Android and iOS return different types. + zIndex: (tileInfo['zIndex']! as num).toInt(), + ); + } + + @override + Future isCompassEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isCompassEnabled'))!; + } + + @override + Future isLiteModeEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isLiteModeEnabled'))!; + } + + @override + Future isMapToolbarEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isMapToolbarEnabled'))!; + } + + @override + Future isMyLocationButtonEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isMyLocationButtonEnabled'))!; + } + + @override + Future isTrafficEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isTrafficEnabled'))!; + } +} 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 new file mode 100644 index 000000000000..06c5bdcd7e0f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart @@ -0,0 +1,691 @@ +// 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(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:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:stream_transform/stream_transform.dart'; + +import 'google_map_inspector_android.dart'; + +// TODO(stuartmorgan): Remove the dependency on platform interface toJson +// methods. Channel serialization details should all be package-internal. + +/// Error thrown when an unknown map ID is provided to a method channel API. +class UnknownMapIDError extends Error { + /// Creates an assertion error with the provided [mapId] and optional + /// [message]. + UnknownMapIDError(this.mapId, [this.message]); + + /// The unknown ID. + final int mapId; + + /// Message describing the assertion error. + final Object? message; + + @override + String toString() { + if (message != null) { + return 'Unknown map ID $mapId: ${Error.safeToString(message)}'; + } + return 'Unknown map ID $mapId'; + } +} + +/// An implementation of [GoogleMapsFlutterPlatform] for Android. +class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { + /// Registers the Android implementation of GoogleMapsFlutterPlatform. + static void registerWith() { + GoogleMapsFlutterPlatform.instance = GoogleMapsFlutterAndroid(); + } + + // Keep a collection of id -> channel + // Every method call passes the int mapId + final Map _channels = {}; + + /// Accesses the MethodChannel associated to the passed mapId. + MethodChannel _channel(int mapId) { + final MethodChannel? channel = _channels[mapId]; + if (channel == null) { + throw UnknownMapIDError(mapId); + } + return channel; + } + + // Keep a collection of mapId to a map of TileOverlays. + final Map> _tileOverlays = + >{}; + + /// Returns the channel for [mapId], creating it if it doesn't already exist. + @visibleForTesting + MethodChannel ensureChannelInitialized(int mapId) { + MethodChannel? channel = _channels[mapId]; + if (channel == null) { + channel = MethodChannel('plugins.flutter.dev/google_maps_android_$mapId'); + channel.setMethodCallHandler( + (MethodCall call) => _handleMethodCall(call, mapId)); + _channels[mapId] = channel; + } + return channel; + } + + @override + Future init(int mapId) { + final MethodChannel channel = ensureChannelInitialized(mapId); + return channel.invokeMethod('map#waitForMap'); + } + + @override + void dispose({required int mapId}) { + // Noop! + } + + // The controller we need to broadcast the different events coming + // from handleMethodCall. + // + // It is a `broadcast` because multiple controllers will connect to + // different stream views of this Controller. + final StreamController> _mapEventStreamController = + StreamController>.broadcast(); + + // Returns a filtered view of the events in the _controller, by mapId. + Stream> _events(int mapId) => + _mapEventStreamController.stream + .where((MapEvent event) => event.mapId == mapId); + + @override + Stream onCameraMoveStarted({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCameraMove({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCameraIdle({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onInfoWindowTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDragStart({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDrag({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDragEnd({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onPolylineTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onPolygonTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCircleTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onLongPress({required int mapId}) { + return _events(mapId).whereType(); + } + + Future _handleMethodCall(MethodCall call, int mapId) async { + switch (call.method) { + case 'camera#onMoveStarted': + _mapEventStreamController.add(CameraMoveStartedEvent(mapId)); + break; + case 'camera#onMove': + _mapEventStreamController.add(CameraMoveEvent( + mapId, + CameraPosition.fromMap(call.arguments['position'])!, + )); + break; + case 'camera#onIdle': + _mapEventStreamController.add(CameraIdleEvent(mapId)); + break; + case 'marker#onTap': + _mapEventStreamController.add(MarkerTapEvent( + mapId, + MarkerId(call.arguments['markerId'] as String), + )); + break; + case 'marker#onDragStart': + _mapEventStreamController.add(MarkerDragStartEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + MarkerId(call.arguments['markerId'] as String), + )); + break; + case 'marker#onDrag': + _mapEventStreamController.add(MarkerDragEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + MarkerId(call.arguments['markerId'] as String), + )); + break; + case 'marker#onDragEnd': + _mapEventStreamController.add(MarkerDragEndEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + MarkerId(call.arguments['markerId'] as String), + )); + break; + case 'infoWindow#onTap': + _mapEventStreamController.add(InfoWindowTapEvent( + mapId, + MarkerId(call.arguments['markerId'] as String), + )); + break; + case 'polyline#onTap': + _mapEventStreamController.add(PolylineTapEvent( + mapId, + PolylineId(call.arguments['polylineId'] as String), + )); + break; + case 'polygon#onTap': + _mapEventStreamController.add(PolygonTapEvent( + mapId, + PolygonId(call.arguments['polygonId'] as String), + )); + break; + case 'circle#onTap': + _mapEventStreamController.add(CircleTapEvent( + mapId, + CircleId(call.arguments['circleId'] as String), + )); + break; + case 'map#onTap': + _mapEventStreamController.add(MapTapEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + )); + break; + case 'map#onLongPress': + _mapEventStreamController.add(MapLongPressEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + )); + break; + case 'tileOverlay#getTile': + final Map? tileOverlaysForThisMap = + _tileOverlays[mapId]; + final String tileOverlayId = call.arguments['tileOverlayId'] as String; + final TileOverlay? tileOverlay = + tileOverlaysForThisMap?[TileOverlayId(tileOverlayId)]; + final TileProvider? tileProvider = tileOverlay?.tileProvider; + if (tileProvider == null) { + return TileProvider.noTile.toJson(); + } + final Tile tile = await tileProvider.getTile( + call.arguments['x'] as int, + call.arguments['y'] as int, + call.arguments['zoom'] as int?, + ); + return tile.toJson(); + default: + throw MissingPluginException(); + } + } + + @override + Future updateMapOptions( + Map optionsUpdate, { + required int mapId, + }) { + assert(optionsUpdate != null); + return _channel(mapId).invokeMethod( + 'map#update', + { + 'options': optionsUpdate, + }, + ); + } + + @override + Future updateMarkers( + MarkerUpdates markerUpdates, { + required int mapId, + }) { + assert(markerUpdates != null); + return _channel(mapId).invokeMethod( + 'markers#update', + markerUpdates.toJson(), + ); + } + + @override + Future updatePolygons( + PolygonUpdates polygonUpdates, { + required int mapId, + }) { + assert(polygonUpdates != null); + return _channel(mapId).invokeMethod( + 'polygons#update', + polygonUpdates.toJson(), + ); + } + + @override + Future updatePolylines( + PolylineUpdates polylineUpdates, { + required int mapId, + }) { + assert(polylineUpdates != null); + return _channel(mapId).invokeMethod( + 'polylines#update', + polylineUpdates.toJson(), + ); + } + + @override + Future updateCircles( + CircleUpdates circleUpdates, { + required int mapId, + }) { + assert(circleUpdates != null); + return _channel(mapId).invokeMethod( + 'circles#update', + circleUpdates.toJson(), + ); + } + + @override + Future updateTileOverlays({ + required Set newTileOverlays, + required int mapId, + }) { + final Map? currentTileOverlays = + _tileOverlays[mapId]; + final Set previousSet = currentTileOverlays != null + ? currentTileOverlays.values.toSet() + : {}; + final _TileOverlayUpdates updates = + _TileOverlayUpdates.from(previousSet, newTileOverlays); + _tileOverlays[mapId] = keyTileOverlayId(newTileOverlays); + return _channel(mapId).invokeMethod( + 'tileOverlays#update', + updates.toJson(), + ); + } + + @override + Future clearTileCache( + TileOverlayId tileOverlayId, { + required int mapId, + }) { + return _channel(mapId) + .invokeMethod('tileOverlays#clearTileCache', { + 'tileOverlayId': tileOverlayId.value, + }); + } + + @override + Future animateCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) { + return _channel(mapId) + .invokeMethod('camera#animate', { + 'cameraUpdate': cameraUpdate.toJson(), + }); + } + + @override + Future moveCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) { + return _channel(mapId).invokeMethod('camera#move', { + 'cameraUpdate': cameraUpdate.toJson(), + }); + } + + @override + Future setMapStyle( + String? mapStyle, { + required int mapId, + }) async { + final List successAndError = (await _channel(mapId) + .invokeMethod>('map#setStyle', mapStyle))!; + final bool success = successAndError[0] as bool; + if (!success) { + throw MapStyleException(successAndError[1] as String); + } + } + + @override + Future getVisibleRegion({ + required int mapId, + }) async { + final Map latLngBounds = (await _channel(mapId) + .invokeMapMethod('map#getVisibleRegion'))!; + final LatLng southwest = LatLng.fromJson(latLngBounds['southwest'])!; + final LatLng northeast = LatLng.fromJson(latLngBounds['northeast'])!; + + return LatLngBounds(northeast: northeast, southwest: southwest); + } + + @override + Future getScreenCoordinate( + LatLng latLng, { + required int mapId, + }) async { + final Map point = (await _channel(mapId) + .invokeMapMethod( + 'map#getScreenCoordinate', latLng.toJson()))!; + + return ScreenCoordinate(x: point['x']!, y: point['y']!); + } + + @override + Future getLatLng( + ScreenCoordinate screenCoordinate, { + required int mapId, + }) async { + final List latLng = (await _channel(mapId) + .invokeMethod>( + 'map#getLatLng', screenCoordinate.toJson()))!; + return LatLng(latLng[0] as double, latLng[1] as double); + } + + @override + Future showMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) { + assert(markerId != null); + return _channel(mapId).invokeMethod( + 'markers#showInfoWindow', {'markerId': markerId.value}); + } + + @override + Future hideMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) { + assert(markerId != null); + return _channel(mapId).invokeMethod( + 'markers#hideInfoWindow', {'markerId': markerId.value}); + } + + @override + Future isMarkerInfoWindowShown( + MarkerId markerId, { + required int mapId, + }) async { + assert(markerId != null); + return (await _channel(mapId).invokeMethod( + 'markers#isInfoWindowShown', + {'markerId': markerId.value}))!; + } + + @override + Future getZoomLevel({ + required int mapId, + }) async { + return (await _channel(mapId).invokeMethod('map#getZoomLevel'))!; + } + + @override + Future takeSnapshot({ + required int mapId, + }) { + return _channel(mapId).invokeMethod('map#takeSnapshot'); + } + + /// Set [GoogleMapsFlutterPlatform] to use [AndroidViewSurface] to build the + /// Google Maps widget. + /// + /// See https://pub.dev/packages/google_maps_flutter_android#display-mode + /// for more information. + /// + /// Currently defaults to true, but the default is subject to change. + bool useAndroidViewSurface = true; + + Widget _buildView( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required MapWidgetConfiguration widgetConfiguration, + MapObjects mapObjects = const MapObjects(), + Map mapOptions = const {}, + }) { + final Map creationParams = { + 'initialCameraPosition': + widgetConfiguration.initialCameraPosition.toMap(), + 'options': mapOptions, + 'markersToAdd': serializeMarkerSet(mapObjects.markers), + 'polygonsToAdd': serializePolygonSet(mapObjects.polygons), + 'polylinesToAdd': serializePolylineSet(mapObjects.polylines), + 'circlesToAdd': serializeCircleSet(mapObjects.circles), + 'tileOverlaysToAdd': serializeTileOverlaySet(mapObjects.tileOverlays), + }; + + const String viewType = 'plugins.flutter.dev/google_maps_android'; + if (useAndroidViewSurface) { + return PlatformViewLink( + viewType: viewType, + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: widgetConfiguration.gestureRecognizers, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + final AndroidViewController controller = + PlatformViewsService.initExpensiveAndroidView( + id: params.id, + viewType: viewType, + layoutDirection: widgetConfiguration.textDirection, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + onFocus: () => params.onFocusChanged(true), + ); + controller.addOnPlatformViewCreatedListener( + params.onPlatformViewCreated, + ); + controller.addOnPlatformViewCreatedListener( + onPlatformViewCreated, + ); + + controller.create(); + return controller; + }, + ); + } else { + return AndroidView( + viewType: viewType, + onPlatformViewCreated: onPlatformViewCreated, + gestureRecognizers: widgetConfiguration.gestureRecognizers, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } + } + + @override + Widget buildViewWithConfiguration( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required MapWidgetConfiguration widgetConfiguration, + MapConfiguration mapConfiguration = const MapConfiguration(), + MapObjects mapObjects = const MapObjects(), + }) { + return _buildView( + creationId, + onPlatformViewCreated, + widgetConfiguration: widgetConfiguration, + mapObjects: mapObjects, + mapOptions: _jsonForMapConfiguration(mapConfiguration), + ); + } + + @override + Widget buildViewWithTextDirection( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + required TextDirection textDirection, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + return _buildView( + creationId, + onPlatformViewCreated, + widgetConfiguration: MapWidgetConfiguration( + initialCameraPosition: initialCameraPosition, + textDirection: textDirection), + mapObjects: MapObjects( + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + tileOverlays: tileOverlays), + mapOptions: mapOptions, + ); + } + + @override + Widget buildView( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + return buildViewWithTextDirection( + creationId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + textDirection: TextDirection.ltr, + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + tileOverlays: tileOverlays, + gestureRecognizers: gestureRecognizers, + mapOptions: mapOptions, + ); + } + + @override + @visibleForTesting + void enableDebugInspection() { + GoogleMapsInspectorPlatform.instance = + GoogleMapsInspectorAndroid((int mapId) => _channel(mapId)); + } +} + +Map _jsonForMapConfiguration(MapConfiguration config) { + final EdgeInsets? padding = config.padding; + return { + if (config.compassEnabled != null) 'compassEnabled': config.compassEnabled!, + if (config.mapToolbarEnabled != null) + 'mapToolbarEnabled': config.mapToolbarEnabled!, + if (config.cameraTargetBounds != null) + 'cameraTargetBounds': config.cameraTargetBounds!.toJson(), + if (config.mapType != null) 'mapType': config.mapType!.index, + if (config.minMaxZoomPreference != null) + 'minMaxZoomPreference': config.minMaxZoomPreference!.toJson(), + if (config.rotateGesturesEnabled != null) + 'rotateGesturesEnabled': config.rotateGesturesEnabled!, + if (config.scrollGesturesEnabled != null) + 'scrollGesturesEnabled': config.scrollGesturesEnabled!, + if (config.tiltGesturesEnabled != null) + 'tiltGesturesEnabled': config.tiltGesturesEnabled!, + if (config.zoomControlsEnabled != null) + 'zoomControlsEnabled': config.zoomControlsEnabled!, + if (config.zoomGesturesEnabled != null) + 'zoomGesturesEnabled': config.zoomGesturesEnabled!, + if (config.liteModeEnabled != null) + 'liteModeEnabled': config.liteModeEnabled!, + if (config.trackCameraPosition != null) + 'trackCameraPosition': config.trackCameraPosition!, + if (config.myLocationEnabled != null) + 'myLocationEnabled': config.myLocationEnabled!, + if (config.myLocationButtonEnabled != null) + 'myLocationButtonEnabled': config.myLocationButtonEnabled!, + if (padding != null) + 'padding': [ + padding.top, + padding.left, + padding.bottom, + padding.right, + ], + if (config.indoorViewEnabled != null) + 'indoorEnabled': config.indoorViewEnabled!, + if (config.trafficEnabled != null) 'trafficEnabled': config.trafficEnabled!, + if (config.buildingsEnabled != null) + 'buildingsEnabled': config.buildingsEnabled!, + }; +} + +/// Update specification for a set of [TileOverlay]s. +// TODO(stuartmorgan): Fix the missing export of this class in the platform +// interface, and remove this copy. +class _TileOverlayUpdates extends MapsObjectUpdates { + /// Computes [TileOverlayUpdates] given previous and current [TileOverlay]s. + _TileOverlayUpdates.from(Set previous, Set current) + : super.from(previous, current, objectName: 'tileOverlay'); + + /// Set of TileOverlays to be added in this update. + Set get tileOverlaysToAdd => objectsToAdd; + + /// Set of TileOverlayIds to be removed in this update. + Set get tileOverlayIdsToRemove => + objectIdsToRemove.cast(); + + /// Set of TileOverlays to be changed in this update. + Set get tileOverlaysToChange => objectsToChange; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml new file mode 100644 index 000000000000..d20322bc6ee1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -0,0 +1,31 @@ +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.3.3 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + implements: google_maps_flutter + platforms: + android: + package: io.flutter.plugins.googlemaps + pluginClass: GoogleMapsPlugin + dartPluginClass: GoogleMapsFlutterAndroid + +dependencies: + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.1 + google_maps_flutter_platform_interface: ^2.2.1 + stream_transform: ^2.0.0 + +dev_dependencies: + async: ^2.5.0 + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.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 new file mode 100644 index 000000000000..431c2472945e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart @@ -0,0 +1,166 @@ +// 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:async/async.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late List log; + + setUp(() async { + log = []; + }); + + /// Initializes a map with the given ID and canned responses, logging all + /// calls to [log]. + void configureMockMap( + GoogleMapsFlutterAndroid maps, { + required int mapId, + required Future? Function(MethodCall call) handler, + }) { + maps + .ensureChannelInitialized(mapId) + .setMockMethodCallHandler((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 + .handlePlatformMessage('plugins.flutter.dev/google_maps_android_$mapId', + byteData, (ByteData? data) {}); + } + + test('registers instance', () async { + GoogleMapsFlutterAndroid.registerWith(); + expect(GoogleMapsFlutterPlatform.instance, isA()); + }); + + // Calls each method that uses invokeMethod with a return type other than + // void to ensure that the casting/nullability handling succeeds. + // + // TODO(stuartmorgan): Remove this once there is real test coverage of + // each method, since that would cover this issue. + test('non-void invokeMethods handle types correctly', () async { + const int mapId = 0; + final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid(); + configureMockMap(maps, mapId: mapId, + handler: (MethodCall methodCall) async { + switch (methodCall.method) { + case 'map#getLatLng': + return [1.0, 2.0]; + case 'markers#isInfoWindowShown': + return true; + case 'map#getZoomLevel': + return 2.5; + case 'map#takeSnapshot': + return null; + } + }); + + await maps.getLatLng(const ScreenCoordinate(x: 0, y: 0), mapId: mapId); + await maps.isMarkerInfoWindowShown(const MarkerId(''), mapId: mapId); + await maps.getZoomLevel(mapId: mapId); + await maps.takeSnapshot(mapId: mapId); + // Check that all the invokeMethod calls happened. + expect(log, [ + 'map#getLatLng', + 'markers#isInfoWindowShown', + 'map#getZoomLevel', + 'map#takeSnapshot', + ]); + }); + + test('markers send drag event to correct streams', () async { + const int mapId = 1; + final Map jsonMarkerDragStartEvent = { + 'mapId': mapId, + 'markerId': 'drag-start-marker', + 'position': [1.0, 1.0] + }; + final Map jsonMarkerDragEvent = { + 'mapId': mapId, + 'markerId': 'drag-marker', + 'position': [1.0, 1.0] + }; + final Map jsonMarkerDragEndEvent = { + 'mapId': mapId, + 'markerId': 'drag-end-marker', + 'position': [1.0, 1.0] + }; + + final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid(); + maps.ensureChannelInitialized(mapId); + + final StreamQueue markerDragStartStream = + StreamQueue(maps.onMarkerDragStart(mapId: mapId)); + final StreamQueue markerDragStream = + StreamQueue(maps.onMarkerDrag(mapId: mapId)); + final StreamQueue markerDragEndStream = + StreamQueue(maps.onMarkerDragEnd(mapId: mapId)); + + await sendPlatformMessage( + mapId, 'marker#onDragStart', jsonMarkerDragStartEvent); + await sendPlatformMessage(mapId, 'marker#onDrag', jsonMarkerDragEvent); + await sendPlatformMessage( + mapId, 'marker#onDragEnd', jsonMarkerDragEndEvent); + + expect((await markerDragStartStream.next).value.value, + equals('drag-start-marker')); + expect((await markerDragStream.next).value.value, equals('drag-marker')); + expect((await markerDragEndStream.next).value.value, + equals('drag-end-marker')); + }); + + test( + 'Does not use PlatformViewLink when using TLHC', + () async { + final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid(); + maps.useAndroidViewSurface = false; + final Widget widget = maps.buildViewWithConfiguration(1, (int _) {}, + widgetConfiguration: const MapWidgetConfiguration( + initialCameraPosition: + CameraPosition(target: LatLng(0, 0), zoom: 1), + textDirection: TextDirection.ltr)); + + expect(widget, isA()); + }, + ); + + testWidgets('Use PlatformViewLink when using surface view', + (WidgetTester tester) async { + final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid(); + maps.useAndroidViewSurface = true; + + final Widget widget = maps.buildViewWithConfiguration(1, (int _) {}, + widgetConfiguration: const MapWidgetConfiguration( + initialCameraPosition: + CameraPosition(target: LatLng(0, 0), zoom: 1), + textDirection: TextDirection.ltr)); + + expect(widget, isA()); + }); + + testWidgets('Defaults to surface view', (WidgetTester tester) async { + final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid(); + + final Widget widget = maps.buildViewWithConfiguration(1, (int _) {}, + widgetConfiguration: const MapWidgetConfiguration( + initialCameraPosition: + CameraPosition(target: LatLng(0, 0), zoom: 1), + textDirection: TextDirection.ltr)); + + expect(widget, isA()); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/AUTHORS b/packages/google_maps_flutter/google_maps_flutter_ios/AUTHORS new file mode 100644 index 000000000000..9f1b53ee2667 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Taha Tesser diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md new file mode 100644 index 000000000000..7788e9146809 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md @@ -0,0 +1,15 @@ +## 2.1.12 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. +* Fixes violations of new analysis option use_named_constants. +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 2.1.11 + +* Precaches Google Maps services initialization and syncing. + +## 2.1.10 + +* Splits iOS implementation out of `google_maps_flutter` as a federated + implementation. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/LICENSE b/packages/google_maps_flutter/google_maps_flutter_ios/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/README.md b/packages/google_maps_flutter/google_maps_flutter_ios/README.md new file mode 100644 index 000000000000..cd5d3f1b7e6e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/README.md @@ -0,0 +1,12 @@ +# google\_maps\_flutter\_ios + +The iOS implementation of [`google_maps_flutter`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use +`google_maps_flutter` normally. This package will be automatically included in +your app when you do. + +[1]: https://pub.dev/packages/google_maps_flutter +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/.metadata b/packages/google_maps_flutter/google_maps_flutter_ios/example/.metadata new file mode 100644 index 000000000000..46e884ce48d1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 3ea4d06340a97a1e9d7cae97567c64e0569dcaa2 + channel: beta diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/README.md b/packages/google_maps_flutter/google_maps_flutter_ios/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/2.0x/red_square.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/2.0x/red_square.png new file mode 100644 index 000000000000..0f82237796bf Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/2.0x/red_square.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/3.0x/red_square.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/3.0x/red_square.png new file mode 100644 index 000000000000..7e2739974e7b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/3.0x/red_square.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/night_mode.json b/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/night_mode.json new file mode 100644 index 000000000000..1f16e003a920 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/night_mode.json @@ -0,0 +1,162 @@ +[ + { + "elementType": "geometry", + "stylers": [ + { + "color": "#242f3e" + } + ] + }, + { + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#746855" + } + ] + }, + { + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#242f3e" + } + ] + }, + { + "featureType": "administrative.locality", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#d59563" + } + ] + }, + { + "featureType": "poi", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#d59563" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "geometry", + "stylers": [ + { + "color": "#263c3f" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#6b9a76" + } + ] + }, + { + "featureType": "road", + "elementType": "geometry", + "stylers": [ + { + "color": "#38414e" + } + ] + }, + { + "featureType": "road", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#212a37" + } + ] + }, + { + "featureType": "road", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#9ca5b3" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [ + { + "color": "#746855" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#1f2835" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#f3d19c" + } + ] + }, + { + "featureType": "transit", + "elementType": "geometry", + "stylers": [ + { + "color": "#2f3948" + } + ] + }, + { + "featureType": "transit.station", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#d59563" + } + ] + }, + { + "featureType": "water", + "elementType": "geometry", + "stylers": [ + { + "color": "#17263c" + } + ] + }, + { + "featureType": "water", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#515c6d" + } + ] + }, + { + "featureType": "water", + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#17263c" + } + ] + } +] + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/red_square.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/red_square.png new file mode 100644 index 000000000000..650a2dee711d Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/red_square.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/integration_test/google_maps_test.dart new file mode 100644 index 000000000000..eb00ccb673f4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/integration_test/google_maps_test.dart @@ -0,0 +1,1071 @@ +// 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:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_example/example_google_map.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +const LatLng _kInitialMapCenter = LatLng(0, 0); +const double _kInitialZoomLevel = 5; +const CameraPosition _kInitialCameraPosition = + CameraPosition(target: _kInitialMapCenter, zoom: _kInitialZoomLevel); + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + GoogleMapsFlutterPlatform.instance.enableDebugInspection(); + + testWidgets('testCompassToggle', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + compassEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool compassEnabled = await inspector.isCompassEnabled(mapId: mapId); + expect(compassEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + compassEnabled = await inspector.isCompassEnabled(mapId: mapId); + expect(compassEnabled, true); + }); + + testWidgets('testMapToolbar returns false', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + final bool mapToolbarEnabled = + await inspector.isMapToolbarEnabled(mapId: mapId); + // This is only supported on Android, so should always return false. + expect(mapToolbarEnabled, false); + }); + + testWidgets('updateMinMaxZoomLevels', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + const MinMaxZoomPreference initialZoomLevel = MinMaxZoomPreference(4, 8); + const MinMaxZoomPreference finalZoomLevel = MinMaxZoomPreference(6, 10); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + minMaxZoomPreference: initialZoomLevel, + onMapCreated: (ExampleGoogleMapController c) async { + controllerCompleter.complete(c); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + MinMaxZoomPreference zoomLevel = + await inspector.getMinMaxZoomLevels(mapId: controller.mapId); + expect(zoomLevel, equals(initialZoomLevel)); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + minMaxZoomPreference: finalZoomLevel, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + zoomLevel = await inspector.getMinMaxZoomLevels(mapId: controller.mapId); + expect(zoomLevel, equals(finalZoomLevel)); + }); + + testWidgets('testZoomGesturesEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + zoomGesturesEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool zoomGesturesEnabled = + await inspector.areZoomGesturesEnabled(mapId: mapId); + expect(zoomGesturesEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + zoomGesturesEnabled = await inspector.areZoomGesturesEnabled(mapId: mapId); + expect(zoomGesturesEnabled, true); + }); + + testWidgets('testZoomControlsEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + final bool zoomControlsEnabled = + await inspector.areZoomControlsEnabled(mapId: mapId); + + /// Zoom Controls functionality is not available on iOS at the moment. + expect(zoomControlsEnabled, false); + }); + + testWidgets('testRotateGesturesEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + rotateGesturesEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool rotateGesturesEnabled = + await inspector.areRotateGesturesEnabled(mapId: mapId); + expect(rotateGesturesEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + rotateGesturesEnabled = + await inspector.areRotateGesturesEnabled(mapId: mapId); + expect(rotateGesturesEnabled, true); + }); + + testWidgets('testTiltGesturesEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tiltGesturesEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool tiltGesturesEnabled = + await inspector.areTiltGesturesEnabled(mapId: mapId); + expect(tiltGesturesEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + tiltGesturesEnabled = await inspector.areTiltGesturesEnabled(mapId: mapId); + expect(tiltGesturesEnabled, true); + }); + + testWidgets('testScrollGesturesEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + scrollGesturesEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool scrollGesturesEnabled = + await inspector.areScrollGesturesEnabled(mapId: mapId); + expect(scrollGesturesEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + scrollGesturesEnabled = + await inspector.areScrollGesturesEnabled(mapId: mapId); + expect(scrollGesturesEnabled, true); + }); + + testWidgets('testInitialCenterLocationAtCenter', (WidgetTester tester) async { + await tester.binding.setSurfaceSize(const Size(800, 600)); + + final Completer mapControllerCompleter = + Completer(); + final Key key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapControllerCompleter.complete(controller); + }, + ), + ), + ); + final ExampleGoogleMapController mapController = + await mapControllerCompleter.future; + + await tester.pumpAndSettle(); + + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + final ScreenCoordinate coordinate = + await mapController.getScreenCoordinate(_kInitialCameraPosition.target); + final Rect rect = tester.getRect(find.byKey(key)); + expect(coordinate.x, (rect.center.dx - rect.topLeft.dx).round()); + expect(coordinate.y, (rect.center.dy - rect.topLeft.dy).round()); + + await tester.binding.setSurfaceSize(null); + }); + + testWidgets('testGetVisibleRegion', (WidgetTester tester) async { + final Key key = GlobalKey(); + final LatLngBounds zeroLatLngBounds = LatLngBounds( + southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)); + + final Completer mapControllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapControllerCompleter.complete(controller); + }, + ), + )); + await tester.pumpAndSettle(); + + final ExampleGoogleMapController mapController = + await mapControllerCompleter.future; + + final LatLngBounds firstVisibleRegion = + await mapController.getVisibleRegion(); + + expect(firstVisibleRegion, isNotNull); + expect(firstVisibleRegion.southwest, isNotNull); + expect(firstVisibleRegion.northeast, isNotNull); + expect(firstVisibleRegion, isNot(zeroLatLngBounds)); + expect(firstVisibleRegion.contains(_kInitialMapCenter), isTrue); + + // Making a new `LatLngBounds` about (10, 10) distance south west to the `firstVisibleRegion`. + // The size of the `LatLngBounds` is 10 by 10. + final LatLng southWest = LatLng(firstVisibleRegion.southwest.latitude - 20, + firstVisibleRegion.southwest.longitude - 20); + final LatLng northEast = LatLng(firstVisibleRegion.southwest.latitude - 10, + firstVisibleRegion.southwest.longitude - 10); + final LatLng newCenter = LatLng( + (northEast.latitude + southWest.latitude) / 2, + (northEast.longitude + southWest.longitude) / 2, + ); + + expect(firstVisibleRegion.contains(northEast), isFalse); + expect(firstVisibleRegion.contains(southWest), isFalse); + + final LatLngBounds latLngBounds = + LatLngBounds(southwest: southWest, northeast: northEast); + + // TODO(iskakaushik): non-zero padding is needed for some device configurations + // https://github.com/flutter/flutter/issues/30575 + const double padding = 0; + await mapController + .moveCamera(CameraUpdate.newLatLngBounds(latLngBounds, padding)); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final LatLngBounds secondVisibleRegion = + await mapController.getVisibleRegion(); + + expect(secondVisibleRegion, isNotNull); + expect(secondVisibleRegion.southwest, isNotNull); + expect(secondVisibleRegion.northeast, isNotNull); + expect(secondVisibleRegion, isNot(zeroLatLngBounds)); + + expect(firstVisibleRegion, isNot(secondVisibleRegion)); + expect(secondVisibleRegion.contains(newCenter), isTrue); + }); + + testWidgets('testTraffic', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + trafficEnabled: true, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool isTrafficEnabled = await inspector.isTrafficEnabled(mapId: mapId); + expect(isTrafficEnabled, true); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + isTrafficEnabled = await inspector.isTrafficEnabled(mapId: mapId); + expect(isTrafficEnabled, false); + }); + + testWidgets('testBuildings', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + final bool isBuildingsEnabled = + await inspector.areBuildingsEnabled(mapId: mapId); + expect(isBuildingsEnabled, true); + }); + + testWidgets('testMyLocationButtonToggle', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + bool myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(mapId: mapId); + expect(myLocationButtonEnabled, true); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + myLocationButtonEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + )); + + myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(mapId: mapId); + expect(myLocationButtonEnabled, false); + }); + + testWidgets('testMyLocationButton initial value false', + (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + myLocationButtonEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + final bool myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(mapId: mapId); + expect(myLocationButtonEnabled, false); + }); + + testWidgets('testMyLocationButton initial value true', + (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer mapIdCompleter = Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + )); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + final bool myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(mapId: mapId); + expect(myLocationButtonEnabled, true); + }); + + testWidgets('testSetMapStyle valid Json String', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + const String mapStyle = + '[{"elementType":"geometry","stylers":[{"color":"#242f3e"}]}]'; + await controller.setMapStyle(mapStyle); + }); + + testWidgets('testSetMapStyle invalid Json String', + (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + try { + await controller.setMapStyle('invalid_value'); + fail('expected MapStyleException'); + } on MapStyleException catch (e) { + expect(e.cause, isNotNull); + } + }); + + testWidgets('testSetMapStyle null string', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + await controller.setMapStyle(null); + }); + + testWidgets('testGetLatLng', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + final LatLngBounds visibleRegion = await controller.getVisibleRegion(); + final LatLng topLeft = + await controller.getLatLng(const ScreenCoordinate(x: 0, y: 0)); + final LatLng northWest = LatLng( + visibleRegion.northeast.latitude, + visibleRegion.southwest.longitude, + ); + + expect(topLeft, northWest); + }); + + testWidgets('testGetZoomLevel', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + double zoom = await controller.getZoomLevel(); + expect(zoom, _kInitialZoomLevel); + + await controller.moveCamera(CameraUpdate.zoomTo(7)); + await tester.pumpAndSettle(); + zoom = await controller.getZoomLevel(); + expect(zoom, equals(7)); + }); + + testWidgets('testScreenCoordinate', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + final LatLngBounds visibleRegion = await controller.getVisibleRegion(); + final LatLng northWest = LatLng( + visibleRegion.northeast.latitude, + visibleRegion.southwest.longitude, + ); + final ScreenCoordinate topLeft = + await controller.getScreenCoordinate(northWest); + expect(topLeft, const ScreenCoordinate(x: 0, y: 0)); + }); + + testWidgets('testResizeWidget', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final ExampleGoogleMap map = ExampleGoogleMap( + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) async { + controllerCompleter.complete(controller); + }, + ); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: MaterialApp( + home: Scaffold( + body: SizedBox(height: 100, width: 100, child: map))))); + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: MaterialApp( + home: Scaffold( + body: SizedBox(height: 400, width: 400, child: map))))); + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + // Simple call to make sure that the app hasn't crashed. + final LatLngBounds bounds1 = await controller.getVisibleRegion(); + final LatLngBounds bounds2 = await controller.getVisibleRegion(); + expect(bounds1, bounds2); + }); + + testWidgets('testToggleInfoWindow', (WidgetTester tester) async { + const Marker marker = Marker( + markerId: MarkerId('marker'), + infoWindow: InfoWindow(title: 'InfoWindow')); + final Set markers = {marker}; + + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + markers: markers, + onMapCreated: (ExampleGoogleMapController googleMapController) { + controllerCompleter.complete(googleMapController); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + bool iwVisibleStatus = + await controller.isMarkerInfoWindowShown(marker.markerId); + expect(iwVisibleStatus, false); + + await controller.showMarkerInfoWindow(marker.markerId); + iwVisibleStatus = await controller.isMarkerInfoWindowShown(marker.markerId); + expect(iwVisibleStatus, true); + + await controller.hideMarkerInfoWindow(marker.markerId); + iwVisibleStatus = await controller.isMarkerInfoWindowShown(marker.markerId); + expect(iwVisibleStatus, false); + }); + + testWidgets('fromAssetImage', (WidgetTester tester) async { + const double pixelRatio = 2; + const ImageConfiguration imageConfiguration = + ImageConfiguration(devicePixelRatio: pixelRatio); + final BitmapDescriptor mip = await BitmapDescriptor.fromAssetImage( + imageConfiguration, 'red_square.png'); + final BitmapDescriptor scaled = await BitmapDescriptor.fromAssetImage( + imageConfiguration, 'red_square.png', + mipmaps: false); + expect((mip.toJson() as List)[2], 1); + expect((scaled.toJson() as List)[2], 2); + }); + + testWidgets('testTakeSnapshot', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + final Uint8List? bytes = await controller.takeSnapshot(); + expect(bytes?.isNotEmpty, true); + }); + + testWidgets( + 'set tileOverlay correctly', + (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 2, + transparency: 0.2, + ); + + final TileOverlay tileOverlay2 = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_2'), + tileProvider: _DebugTileProvider(), + zIndex: 1, + visible: false, + transparency: 0.3, + fadeIn: false, + ); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1, tileOverlay2}, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + final TileOverlay tileOverlayInfo1 = (await inspector + .getTileOverlayInfo(tileOverlay1.mapsId, mapId: mapId))!; + final TileOverlay tileOverlayInfo2 = (await inspector + .getTileOverlayInfo(tileOverlay2.mapsId, mapId: mapId))!; + + expect(tileOverlayInfo1.visible, isTrue); + expect(tileOverlayInfo1.fadeIn, isTrue); + expect( + tileOverlayInfo1.transparency, moreOrLessEquals(0.2, epsilon: 0.001)); + expect(tileOverlayInfo1.zIndex, 2); + + expect(tileOverlayInfo2.visible, isFalse); + expect(tileOverlayInfo2.fadeIn, isFalse); + expect( + tileOverlayInfo2.transparency, moreOrLessEquals(0.3, epsilon: 0.001)); + expect(tileOverlayInfo2.zIndex, 1); + }, + ); + + testWidgets( + 'update tileOverlays correctly', + (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Key key = GlobalKey(); + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 2, + transparency: 0.2, + ); + + final TileOverlay tileOverlay2 = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_2'), + tileProvider: _DebugTileProvider(), + zIndex: 3, + transparency: 0.5, + ); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1, tileOverlay2}, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + final TileOverlay tileOverlay1New = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 1, + visible: false, + transparency: 0.3, + fadeIn: false, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1New}, + onMapCreated: (ExampleGoogleMapController controller) { + fail('update: OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final TileOverlay tileOverlayInfo1 = (await inspector + .getTileOverlayInfo(tileOverlay1.mapsId, mapId: mapId))!; + final TileOverlay? tileOverlayInfo2 = + await inspector.getTileOverlayInfo(tileOverlay2.mapsId, mapId: mapId); + + expect(tileOverlayInfo1.visible, isFalse); + expect(tileOverlayInfo1.fadeIn, isFalse); + expect( + tileOverlayInfo1.transparency, moreOrLessEquals(0.3, epsilon: 0.001)); + expect(tileOverlayInfo1.zIndex, 1); + + expect(tileOverlayInfo2, isNull); + }, + ); + + testWidgets( + 'remove tileOverlays correctly', + (WidgetTester tester) async { + final Completer mapIdCompleter = Completer(); + final Key key = GlobalKey(); + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 2, + transparency: 0.2, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1}, + onMapCreated: (ExampleGoogleMapController controller) { + mapIdCompleter.complete(controller.mapId); + }, + ), + ), + ); + + final int mapId = await mapIdCompleter.future; + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (ExampleGoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + final TileOverlay? tileOverlayInfo1 = + await inspector.getTileOverlayInfo(tileOverlay1.mapsId, mapId: mapId); + + expect(tileOverlayInfo1, isNull); + }, + ); +} + +class _DebugTileProvider implements TileProvider { + _DebugTileProvider() { + boxPaint.isAntiAlias = true; + boxPaint.color = Colors.blue; + boxPaint.strokeWidth = 2.0; + boxPaint.style = PaintingStyle.stroke; + } + + static const int width = 100; + static const int height = 100; + static final Paint boxPaint = Paint(); + static const TextStyle textStyle = TextStyle( + color: Colors.red, + fontSize: 20, + ); + + @override + Future getTile(int x, int y, int? zoom) async { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final TextSpan textSpan = TextSpan( + text: '$x,$y', + style: textStyle, + ); + final TextPainter textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + ); + textPainter.layout( + maxWidth: width.toDouble(), + ); + textPainter.paint(canvas, Offset.zero); + canvas.drawRect( + Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint); + final ui.Picture picture = recorder.endRecording(); + final Uint8List byteData = await picture + .toImage(width, height) + .then((ui.Image image) => + image.toByteData(format: ui.ImageByteFormat.png)) + .then((ByteData? byteData) => byteData!.buffer.asUint8List()); + return Tile(width, height, byteData); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 9.0 + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/Debug.xcconfig b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..e8efba114687 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/Release.xcconfig b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..399e9340e6f6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Podfile b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Podfile new file mode 100644 index 000000000000..14b4bdc51c96 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Podfile @@ -0,0 +1,49 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + + pod 'OCMock', '~> 3.9.1' + end + target 'RunnerUITests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |build_configuration| + build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' + end + end +end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..343e0504134c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,789 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */; }; + 4A097997B7B27CE82FFC3AB8 /* libPods-RunnerUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DC8ED0578E8D540BBDA17645 /* libPods-RunnerUITests.a */; }; + 6851F3562835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */; }; + 68E4726A2836FF0C00BDDDAC /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68E472692836FF0C00BDDDAC /* MapKit.framework */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */ = {isa = PBXBuildFile; fileRef = 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */; }; + F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */; }; + F7151F21265D7EE50028CB91 /* GoogleMapsUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F20265D7EE50028CB91 /* GoogleMapsUITests.m */; }; + FC8F35FC8CD533B128950487 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F7151F15265D7ED70028CB91 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + F7151F23265D7EE50028CB91 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTGoogleMapJSONConversionsConversionTests.m; sourceTree = ""; }; + 68E472692836FF0C00BDDDAC /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.0.sdk/System/iOSSupport/System/Library/Frameworks/MapKit.framework; sourceTree = DEVELOPER_DIR; }; + 6AC1E6095B09DE4B02ECF64E /* Pods-RunnerUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerUITests/Pods-RunnerUITests.release.xcconfig"; sourceTree = ""; }; + 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PartiallyMockedMapView.h; sourceTree = ""; }; + 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PartiallyMockedMapView.m; sourceTree = ""; }; + B7AFC65E3DD5AC60D834D83D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + DC8ED0578E8D540BBDA17645 /* libPods-RunnerUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + DDDAC1342ABDF2F125577581 /* Pods-RunnerUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerUITests/Pods-RunnerUITests.debug.xcconfig"; sourceTree = ""; }; + E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + EA0E91726245EDC22B97E8B9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F10265D7ED70028CB91 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsTests.m; sourceTree = ""; }; + F7151F14265D7ED70028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F7151F1E265D7EE50028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F20265D7EE50028CB91 /* GoogleMapsUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsUITests.m; sourceTree = ""; }; + F7151F22265D7EE50028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F0D265D7ED70028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 68E4726A2836FF0C00BDDDAC /* MapKit.framework in Frameworks */, + FC8F35FC8CD533B128950487 /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F1B265D7EE50028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4A097997B7B27CE82FFC3AB8 /* libPods-RunnerUITests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1E7CF0857EFC88FC263CF3B2 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 68E472692836FF0C00BDDDAC /* MapKit.framework */, + 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */, + F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */, + DC8ED0578E8D540BBDA17645 /* libPods-RunnerUITests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + F7151F11265D7ED70028CB91 /* RunnerTests */, + F7151F1F265D7EE50028CB91 /* RunnerUITests */, + 97C146EF1CF9000F007C117D /* Products */, + A189CFE5474BF8A07908B2E0 /* Pods */, + 1E7CF0857EFC88FC263CF3B2 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + F7151F10265D7ED70028CB91 /* RunnerTests.xctest */, + F7151F1E265D7EE50028CB91 /* RunnerUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + A189CFE5474BF8A07908B2E0 /* Pods */ = { + isa = PBXGroup; + children = ( + B7AFC65E3DD5AC60D834D83D /* Pods-Runner.debug.xcconfig */, + EA0E91726245EDC22B97E8B9 /* Pods-Runner.release.xcconfig */, + E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */, + 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */, + DDDAC1342ABDF2F125577581 /* Pods-RunnerUITests.debug.xcconfig */, + 6AC1E6095B09DE4B02ECF64E /* Pods-RunnerUITests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + F7151F11265D7ED70028CB91 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */, + F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */, + 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */, + 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */, + F7151F14265D7ED70028CB91 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + F7151F1F265D7EE50028CB91 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + F7151F20265D7EE50028CB91 /* GoogleMapsUITests.m */, + F7151F22265D7EE50028CB91 /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 74BF216DF17B0C7F983459BD /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + BB6BD9A1101E970BEF85B6D2 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + F7151F0F265D7ED70028CB91 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F19265D7ED70028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + D067548A17DC238B80D2BD12 /* [CP] Check Pods Manifest.lock */, + F7151F0C265D7ED70028CB91 /* Sources */, + F7151F0D265D7ED70028CB91 /* Frameworks */, + F7151F0E265D7ED70028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F16265D7ED70028CB91 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = F7151F10265D7ED70028CB91 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F7151F1D265D7EE50028CB91 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F25265D7EE50028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + BD39F60794E9A0264D5D3752 /* [CP] Check Pods Manifest.lock */, + F7151F1A265D7EE50028CB91 /* Sources */, + F7151F1B265D7EE50028CB91 /* Frameworks */, + F7151F1C265D7EE50028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F24265D7EE50028CB91 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = F7151F1E265D7EE50028CB91 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1320; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + F7151F0F265D7ED70028CB91 = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + F7151F1D265D7EE50028CB91 = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + F7151F0F265D7ED70028CB91 /* RunnerTests */, + F7151F1D265D7EE50028CB91 /* RunnerUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F0E265D7ED70028CB91 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F1C265D7EE50028CB91 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 74BF216DF17B0C7F983459BD /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + BB6BD9A1101E970BEF85B6D2 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + BD39F60794E9A0264D5D3752 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + D067548A17DC238B80D2BD12 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F0C265D7ED70028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */, + 6851F3562835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m in Sources */, + 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F1A265D7EE50028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F21265D7EE50028CB91 /* GoogleMapsUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F7151F16265D7ED70028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F15265D7ED70028CB91 /* PBXContainerItemProxy */; + }; + F7151F24265D7EE50028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F23265D7EE50028CB91 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.googleMobileMapsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.googleMobileMapsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + F7151F17265D7ED70028CB91 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + F7151F18265D7ED70028CB91 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + F7151F26265D7EE50028CB91 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DDDAC1342ABDF2F125577581 /* Pods-RunnerUITests.debug.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + 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)"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + F7151F27265D7EE50028CB91 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6AC1E6095B09DE4B02ECF64E /* Pods-RunnerUITests.release.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + 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)"; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F7151F19265D7ED70028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F17265D7ED70028CB91 /* Debug */, + F7151F18265D7ED70028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F7151F25265D7EE50028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F26265D7EE50028CB91 /* Debug */, + F7151F27265D7EE50028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..c983bfc640ff --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/AppDelegate.h b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..9bc6c56e34f9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/AppDelegate.h @@ -0,0 +1,9 @@ +// 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 + +@interface AppDelegate : FlutterAppDelegate +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/AppDelegate.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..55733442b4cf --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/AppDelegate.m @@ -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 "AppDelegate.h" +#import "GeneratedPluginRegistrant.h" + +@import GoogleMaps; + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Provide the GoogleMaps API key. + NSString *mapsApiKey = [[NSProcessInfo processInfo] environment][@"MAPS_API_KEY"]; + if ([mapsApiKey length] == 0) { + mapsApiKey = @"YOUR KEY HERE"; + } + [GMSServices provideAPIKey:mapsApiKey]; + + // Register Flutter plugins. + [GeneratedPluginRegistrant registerWithRegistry:self]; + + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000000..3d43d11e66f4 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..0fa9c73c5d42 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + google_maps_flutter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSLocationWhenInUseUsageDescription + This app needs your location to test the location feature of the Google Maps plugin. + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/main.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/main.m @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/FLTGoogleMapJSONConversionsConversionTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/FLTGoogleMapJSONConversionsConversionTests.m new file mode 100644 index 000000000000..bb9020d983c4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/FLTGoogleMapJSONConversionsConversionTests.m @@ -0,0 +1,290 @@ +// 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 google_maps_flutter_ios; +@import google_maps_flutter_ios.Test; +@import XCTest; +@import MapKit; +@import GoogleMaps; + +#import +#import "PartiallyMockedMapView.h" + +@interface FLTGoogleMapJSONConversionsTests : XCTestCase +@end + +@implementation FLTGoogleMapJSONConversionsTests + +- (void)testLocationFromLatLong { + NSArray *latlong = @[ @1, @2 ]; + CLLocationCoordinate2D location = [FLTGoogleMapJSONConversions locationFromLatLong:latlong]; + XCTAssertEqual(location.latitude, 1); + XCTAssertEqual(location.longitude, 2); +} + +- (void)testPointFromArray { + NSArray *array = @[ @1, @2 ]; + CGPoint point = [FLTGoogleMapJSONConversions pointFromArray:array]; + XCTAssertEqual(point.x, 1); + XCTAssertEqual(point.y, 2); +} + +- (void)testArrayFromLocation { + CLLocationCoordinate2D location = CLLocationCoordinate2DMake(1, 2); + NSArray *array = [FLTGoogleMapJSONConversions arrayFromLocation:location]; + XCTAssertEqual([array[0] integerValue], 1); + XCTAssertEqual([array[1] integerValue], 2); +} + +- (void)testColorFromRGBA { + NSNumber *rgba = @(0x01020304); + UIColor *color = [FLTGoogleMapJSONConversions colorFromRGBA:rgba]; + CGFloat red, green, blue, alpha; + BOOL success = [color getRed:&red green:&green blue:&blue alpha:&alpha]; + XCTAssertTrue(success); + const CGFloat accuracy = 0.0001; + XCTAssertEqualWithAccuracy(red, 2 / 255.0, accuracy); + XCTAssertEqualWithAccuracy(green, 3 / 255.0, accuracy); + XCTAssertEqualWithAccuracy(blue, 4 / 255.0, accuracy); + XCTAssertEqualWithAccuracy(alpha, 1 / 255.0, accuracy); +} + +- (void)testPointsFromLatLongs { + NSArray *latlongs = @[ @[ @1, @2 ], @[ @(3), @(4) ] ]; + NSArray *locations = [FLTGoogleMapJSONConversions pointsFromLatLongs:latlongs]; + XCTAssertEqual(locations.count, 2); + XCTAssertEqual(locations[0].coordinate.latitude, 1); + XCTAssertEqual(locations[0].coordinate.longitude, 2); + XCTAssertEqual(locations[1].coordinate.latitude, 3); + XCTAssertEqual(locations[1].coordinate.longitude, 4); +} + +- (void)testHolesFromPointsArray { + NSArray *pointsArray = + @[ @[ @[ @1, @2 ], @[ @(3), @(4) ] ], @[ @[ @(5), @(6) ], @[ @(7), @(8) ] ] ]; + NSArray *> *holes = + [FLTGoogleMapJSONConversions holesFromPointsArray:pointsArray]; + XCTAssertEqual(holes.count, 2); + XCTAssertEqual(holes[0][0].coordinate.latitude, 1); + XCTAssertEqual(holes[0][0].coordinate.longitude, 2); + XCTAssertEqual(holes[0][1].coordinate.latitude, 3); + XCTAssertEqual(holes[0][1].coordinate.longitude, 4); + XCTAssertEqual(holes[1][0].coordinate.latitude, 5); + XCTAssertEqual(holes[1][0].coordinate.longitude, 6); + XCTAssertEqual(holes[1][1].coordinate.latitude, 7); + XCTAssertEqual(holes[1][1].coordinate.longitude, 8); +} + +- (void)testDictionaryFromPosition { + id mockPosition = OCMClassMock([GMSCameraPosition class]); + NSValue *locationValue = [NSValue valueWithMKCoordinate:CLLocationCoordinate2DMake(1, 2)]; + [(GMSCameraPosition *)[[mockPosition stub] andReturnValue:locationValue] target]; + [[[mockPosition stub] andReturnValue:@(2.0)] zoom]; + [[[mockPosition stub] andReturnValue:@(3.0)] bearing]; + [[[mockPosition stub] andReturnValue:@(75.0)] viewingAngle]; + NSDictionary *dictionary = [FLTGoogleMapJSONConversions dictionaryFromPosition:mockPosition]; + NSArray *targetArray = @[ @1, @2 ]; + XCTAssertEqualObjects(dictionary[@"target"], targetArray); + XCTAssertEqualObjects(dictionary[@"zoom"], @2.0); + XCTAssertEqualObjects(dictionary[@"bearing"], @3.0); + XCTAssertEqualObjects(dictionary[@"tilt"], @75.0); +} + +- (void)testDictionaryFromPoint { + CGPoint point = CGPointMake(10, 20); + NSDictionary *dictionary = [FLTGoogleMapJSONConversions dictionaryFromPoint:point]; + const CGFloat accuracy = 0.0001; + XCTAssertEqualWithAccuracy([dictionary[@"x"] floatValue], point.x, accuracy); + XCTAssertEqualWithAccuracy([dictionary[@"y"] floatValue], point.y, accuracy); +} + +- (void)testDictionaryFromCoordinateBounds { + XCTAssertNil([FLTGoogleMapJSONConversions dictionaryFromCoordinateBounds:nil]); + + GMSCoordinateBounds *bounds = + [[GMSCoordinateBounds alloc] initWithCoordinate:CLLocationCoordinate2DMake(10, 20) + coordinate:CLLocationCoordinate2DMake(30, 40)]; + NSDictionary *dictionary = [FLTGoogleMapJSONConversions dictionaryFromCoordinateBounds:bounds]; + NSArray *southwest = @[ @10, @20 ]; + NSArray *northeast = @[ @30, @40 ]; + XCTAssertEqualObjects(dictionary[@"southwest"], southwest); + XCTAssertEqualObjects(dictionary[@"northeast"], northeast); +} + +- (void)testCameraPostionFromDictionary { + XCTAssertNil([FLTGoogleMapJSONConversions cameraPostionFromDictionary:nil]); + + NSDictionary *channelValue = + @{@"target" : @[ @1, @2 ], @"zoom" : @3, @"bearing" : @4, @"tilt" : @5}; + + GMSCameraPosition *cameraPosition = + [FLTGoogleMapJSONConversions cameraPostionFromDictionary:channelValue]; + + const CGFloat accuracy = 0.001; + XCTAssertEqualWithAccuracy(cameraPosition.target.latitude, 1, accuracy); + XCTAssertEqualWithAccuracy(cameraPosition.target.longitude, 2, accuracy); + XCTAssertEqualWithAccuracy(cameraPosition.zoom, 3, accuracy); + XCTAssertEqualWithAccuracy(cameraPosition.bearing, 4, accuracy); + XCTAssertEqualWithAccuracy(cameraPosition.viewingAngle, 5, accuracy); +} + +- (void)testPointFromDictionary { + XCTAssertNil([FLTGoogleMapJSONConversions cameraPostionFromDictionary:nil]); + + NSDictionary *dictionary = @{ + @"x" : @1, + @"y" : @2, + }; + + CGPoint point = [FLTGoogleMapJSONConversions pointFromDictionary:dictionary]; + + const CGFloat accuracy = 0.001; + XCTAssertEqualWithAccuracy(point.x, 1, accuracy); + XCTAssertEqualWithAccuracy(point.y, 2, accuracy); +} + +- (void)testCoordinateBoundsFromLatLongs { + NSArray *latlong1 = @[ @1, @2 ]; + NSArray *latlong2 = @[ @(3), @(4) ]; + + GMSCoordinateBounds *bounds = + [FLTGoogleMapJSONConversions coordinateBoundsFromLatLongs:@[ latlong1, latlong2 ]]; + + const CGFloat accuracy = 0.001; + XCTAssertEqualWithAccuracy(bounds.southWest.latitude, 1, accuracy); + XCTAssertEqualWithAccuracy(bounds.southWest.longitude, 2, accuracy); + XCTAssertEqualWithAccuracy(bounds.northEast.latitude, 3, accuracy); + XCTAssertEqualWithAccuracy(bounds.northEast.longitude, 4, accuracy); +} + +- (void)testMapViewTypeFromTypeValue { + XCTAssertEqual(kGMSTypeNormal, [FLTGoogleMapJSONConversions mapViewTypeFromTypeValue:@1]); + XCTAssertEqual(kGMSTypeSatellite, [FLTGoogleMapJSONConversions mapViewTypeFromTypeValue:@2]); + XCTAssertEqual(kGMSTypeTerrain, [FLTGoogleMapJSONConversions mapViewTypeFromTypeValue:@3]); + XCTAssertEqual(kGMSTypeHybrid, [FLTGoogleMapJSONConversions mapViewTypeFromTypeValue:@4]); + XCTAssertEqual(kGMSTypeNone, [FLTGoogleMapJSONConversions mapViewTypeFromTypeValue:@5]); +} + +- (void)testCameraUpdateFromChannelValueNewCameraPosition { + NSArray *channelValue = @[ + @"newCameraPosition", @{@"target" : @[ @1, @2 ], @"zoom" : @3, @"bearing" : @4, @"tilt" : @5} + ]; + id classMockCameraUpdate = OCMClassMock([GMSCameraUpdate class]); + [FLTGoogleMapJSONConversions cameraUpdateFromChannelValue:channelValue]; + [[classMockCameraUpdate expect] + setCamera:[FLTGoogleMapJSONConversions cameraPostionFromDictionary:channelValue[1]]]; + [classMockCameraUpdate stopMocking]; +} + +// TODO(cyanglaz): Fix the test for CameraUpdateFromChannelValue with the "NewLatlng" key. +// 2 approaches have been tried and neither worked for the tests. +// +// 1. Use OCMock to vefiry that [GMSCameraUpdate setTarget:] is triggered with the correct value. +// This class method conflicts with certain category method in OCMock, causing OCMock not able to +// disambigious them. +// +// 2. Directly verify the GMSCameraUpdate object returned by the method. +// The GMSCameraUpdate object returned from the method doesn't have any accessors to the "target" +// property. It can be used to update the "camera" property in GMSMapView. However, [GMSMapView +// moveCamera:] doesn't update the camera immediately. Thus the GMSCameraUpdate object cannot be +// verified. +// +// The code in below test uses the 2nd approach. +- (void)skip_testCameraUpdateFromChannelValueNewLatLong { + NSArray *channelValue = @[ @"newLatLng", @[ @1, @2 ] ]; + + GMSCameraUpdate *update = [FLTGoogleMapJSONConversions cameraUpdateFromChannelValue:channelValue]; + + GMSMapView *mapView = [[GMSMapView alloc] + initWithFrame:CGRectZero + camera:[GMSCameraPosition cameraWithTarget:CLLocationCoordinate2DMake(5, 6) zoom:1]]; + [mapView moveCamera:update]; + const CGFloat accuracy = 0.001; + XCTAssertEqualWithAccuracy(mapView.camera.target.latitude, 1, + accuracy); // mapView.camera.target.latitude is still 5. + XCTAssertEqualWithAccuracy(mapView.camera.target.longitude, 2, + accuracy); // mapView.camera.target.longitude is still 6. +} + +- (void)testCameraUpdateFromChannelValueNewLatLngBounds { + NSArray *latlong1 = @[ @1, @2 ]; + NSArray *latlong2 = @[ @(3), @(4) ]; + GMSCoordinateBounds *bounds = + [FLTGoogleMapJSONConversions coordinateBoundsFromLatLongs:@[ latlong1, latlong2 ]]; + + NSArray *channelValue = @[ @"newLatLngBounds", @[ latlong1, latlong2 ], @20 ]; + id classMockCameraUpdate = OCMClassMock([GMSCameraUpdate class]); + [FLTGoogleMapJSONConversions cameraUpdateFromChannelValue:channelValue]; + + [[classMockCameraUpdate expect] fitBounds:bounds withPadding:20]; + [classMockCameraUpdate stopMocking]; +} + +- (void)testCameraUpdateFromChannelValueNewLatLngZoom { + NSArray *channelValue = @[ @"newLatLngZoom", @[ @1, @2 ], @3 ]; + + id classMockCameraUpdate = OCMClassMock([GMSCameraUpdate class]); + [FLTGoogleMapJSONConversions cameraUpdateFromChannelValue:channelValue]; + + [[classMockCameraUpdate expect] setTarget:CLLocationCoordinate2DMake(1, 2) zoom:3]; + [classMockCameraUpdate stopMocking]; +} + +- (void)testCameraUpdateFromChannelValueScrollBy { + NSArray *channelValue = @[ @"scrollBy", @1, @2 ]; + + id classMockCameraUpdate = OCMClassMock([GMSCameraUpdate class]); + [FLTGoogleMapJSONConversions cameraUpdateFromChannelValue:channelValue]; + + [[classMockCameraUpdate expect] scrollByX:1 Y:2]; + [classMockCameraUpdate stopMocking]; +} + +- (void)testCameraUpdateFromChannelValueZoomBy { + NSArray *channelValueNoPoint = @[ @"zoomBy", @1 ]; + + id classMockCameraUpdate = OCMClassMock([GMSCameraUpdate class]); + [FLTGoogleMapJSONConversions cameraUpdateFromChannelValue:channelValueNoPoint]; + + [[classMockCameraUpdate expect] zoomBy:1]; + + NSArray *channelValueWithPoint = @[ @"zoomBy", @1, @[ @2, @3 ] ]; + + [FLTGoogleMapJSONConversions cameraUpdateFromChannelValue:channelValueWithPoint]; + + [[classMockCameraUpdate expect] zoomBy:1 atPoint:CGPointMake(2, 3)]; + [classMockCameraUpdate stopMocking]; +} + +- (void)testCameraUpdateFromChannelValueZoomIn { + NSArray *channelValueNoPoint = @[ @"zoomIn" ]; + + id classMockCameraUpdate = OCMClassMock([GMSCameraUpdate class]); + [FLTGoogleMapJSONConversions cameraUpdateFromChannelValue:channelValueNoPoint]; + + [[classMockCameraUpdate expect] zoomIn]; + [classMockCameraUpdate stopMocking]; +} + +- (void)testCameraUpdateFromChannelValueZoomOut { + NSArray *channelValueNoPoint = @[ @"zoomOut" ]; + + id classMockCameraUpdate = OCMClassMock([GMSCameraUpdate class]); + [FLTGoogleMapJSONConversions cameraUpdateFromChannelValue:channelValueNoPoint]; + + [[classMockCameraUpdate expect] zoomOut]; + [classMockCameraUpdate stopMocking]; +} + +- (void)testCameraUpdateFromChannelValueZoomTo { + NSArray *channelValueNoPoint = @[ @"zoomTo", @1 ]; + + id classMockCameraUpdate = OCMClassMock([GMSCameraUpdate class]); + [FLTGoogleMapJSONConversions cameraUpdateFromChannelValue:channelValueNoPoint]; + + [[classMockCameraUpdate expect] zoomTo:1]; + [classMockCameraUpdate stopMocking]; +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/GoogleMapsTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/GoogleMapsTests.m new file mode 100644 index 000000000000..71f1162890b4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/GoogleMapsTests.m @@ -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. + +@import google_maps_flutter_ios; +@import google_maps_flutter_ios.Test; +@import XCTest; +@import GoogleMaps; + +#import +#import "PartiallyMockedMapView.h" + +@interface FLTGoogleMapFactory (Test) +@property(strong, nonatomic, readonly) id sharedMapServices; +@end + +@interface GoogleMapsTests : XCTestCase +@end + +@implementation GoogleMapsTests + +- (void)testPlugin { + FLTGoogleMapsPlugin *plugin = [[FLTGoogleMapsPlugin alloc] init]; + XCTAssertNotNil(plugin); +} + +- (void)testFrameObserver { + id registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + CGRect frame = CGRectMake(0, 0, 100, 100); + PartiallyMockedMapView *mapView = [[PartiallyMockedMapView alloc] + initWithFrame:frame + camera:[[GMSCameraPosition alloc] initWithLatitude:0 longitude:0 zoom:0]]; + FLTGoogleMapController *controller = [[FLTGoogleMapController alloc] initWithMapView:mapView + viewIdentifier:0 + arguments:nil + registrar:registrar]; + + for (NSInteger i = 0; i < 10; ++i) { + [controller view]; + } + XCTAssertEqual(mapView.frameObserverCount, 1); + + mapView.frame = frame; + XCTAssertEqual(mapView.frameObserverCount, 0); +} + +- (void)testMapsServiceSync { + id registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + FLTGoogleMapFactory *factory1 = [[FLTGoogleMapFactory alloc] initWithRegistrar:registrar]; + XCTAssertNotNil(factory1.sharedMapServices); + FLTGoogleMapFactory *factory2 = [[FLTGoogleMapFactory alloc] initWithRegistrar:registrar]; + // Test pointer equality, should be same retained singleton +[GMSServices sharedServices] object. + // Retaining the opaque object should be enough to avoid multiple internal initializations, + // but don't test the internals of the GoogleMaps API. Assume that it does what is documented. + // https://developers.google.com/maps/documentation/ios-sdk/reference/interface_g_m_s_services#a436e03c32b1c0be74e072310a7158831 + XCTAssertEqual(factory1.sharedMapServices, factory2.sharedMapServices); +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/Info.plist rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/Info.plist diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/PartiallyMockedMapView.h b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/PartiallyMockedMapView.h new file mode 100644 index 000000000000..4288401cf90d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/PartiallyMockedMapView.h @@ -0,0 +1,17 @@ +// 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 GoogleMaps; + +/** + * Defines a map view used for testing key-value observing. + */ +@interface PartiallyMockedMapView : GMSMapView + +/** + * The number of times that the `frame` KVO has been added. + */ +@property(nonatomic, assign, readonly) NSInteger frameObserverCount; + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/PartiallyMockedMapView.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/PartiallyMockedMapView.m new file mode 100644 index 000000000000..202a18d128c0 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/PartiallyMockedMapView.m @@ -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 "PartiallyMockedMapView.h" + +@interface PartiallyMockedMapView () + +@property(nonatomic, assign) NSInteger frameObserverCount; + +@end + +@implementation PartiallyMockedMapView + +- (void)addObserver:(NSObject *)observer + forKeyPath:(NSString *)keyPath + options:(NSKeyValueObservingOptions)options + context:(void *)context { + [super addObserver:observer forKeyPath:keyPath options:options context:context]; + + if ([keyPath isEqualToString:@"frame"]) { + ++self.frameObserverCount; + } +} + +- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath { + [super removeObserver:observer forKeyPath:keyPath]; + + if ([keyPath isEqualToString:@"frame"]) { + --self.frameObserverCount; + } +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerUITests/GoogleMapsUITests.m similarity index 58% rename from packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.m rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerUITests/GoogleMapsUITests.m index 2f0c0fa6d615..f4cdb7c50ab2 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerUITests/GoogleMapsUITests.m @@ -4,6 +4,7 @@ @import XCTest; @import os.log; +@import GoogleMaps; @interface GoogleMapsUITests : XCTestCase @property(nonatomic, strong) XCUIApplication *app; @@ -81,12 +82,80 @@ - (void)testMapCoordinatesPage { XCTFail(@"Failed due to not able to find platform view"); } - XCUIElement *getVisibleRegionBoundsButton = app.buttons[@"Get Visible Region Bounds"]; - if (![getVisibleRegionBoundsButton waitForExistenceWithTimeout:30.0]) { + XCUIElement *titleBar = app.otherElements[@"Map coordinates"]; + if (![titleBar waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); - XCTFail(@"Failed due to not able to find 'Get Visible Region Bounds''"); + XCTFail(@"Failed due to not able to find title bar"); } - [getVisibleRegionBoundsButton tap]; + + NSPredicate *visibleRegionPredicate = + [NSPredicate predicateWithFormat:@"label BEGINSWITH 'VisibleRegion'"]; + XCUIElement *visibleRegionText = + [app.staticTexts elementMatchingPredicate:visibleRegionPredicate]; + if (![visibleRegionText waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Visible Region label'"); + } + + // Validate visible region does not change when scrolled under safe areas. + // https://github.com/flutter/flutter/issues/107913 + + // Example -33.79495661816674, 151.313996873796 + CLLocationCoordinate2D originalNortheast; + // Example -33.90900557679571, 151.10800322145224 + CLLocationCoordinate2D originalSouthwest; + [self validateVisibleRegion:visibleRegionText.label + northeast:&originalNortheast + southwest:&originalSouthwest]; + XCTAssertGreaterThan(originalNortheast.latitude, originalSouthwest.latitude); + XCTAssertGreaterThan(originalNortheast.longitude, originalSouthwest.longitude); + + XCTAssertLessThan(originalNortheast.latitude, 0); + XCTAssertLessThan(originalSouthwest.latitude, 0); + XCTAssertGreaterThan(originalNortheast.longitude, 0); + XCTAssertGreaterThan(originalSouthwest.longitude, 0); + + // Drag the map upward to under the title bar. + [platformView pressForDuration:0 thenDragToElement:titleBar]; + + CLLocationCoordinate2D draggedNortheast; + CLLocationCoordinate2D draggedSouthwest; + [self validateVisibleRegion:visibleRegionText.label + northeast:&draggedNortheast + southwest:&draggedSouthwest]; + XCTAssertEqual(originalNortheast.latitude, draggedNortheast.latitude); + XCTAssertEqual(originalNortheast.longitude, draggedNortheast.longitude); + XCTAssertEqual(originalSouthwest.latitude, draggedSouthwest.latitude); + XCTAssertEqual(originalSouthwest.latitude, draggedSouthwest.latitude); +} + +- (void)validateVisibleRegion:(NSString *)label + northeast:(CLLocationCoordinate2D *)northeast + southwest:(CLLocationCoordinate2D *)southwest { + // String will be "VisibleRegion:\nnortheast: LatLng(-33.79495661816674, + // 151.313996873796),\nsouthwest: LatLng(-33.90900557679571, 151.10800322145224)" + NSScanner *scan = [NSScanner scannerWithString:label]; + + // northeast + [scan scanString:@"VisibleRegion:\nnortheast: LatLng(" intoString:NULL]; + double northeastLatitude; + [scan scanDouble:&northeastLatitude]; + [scan scanString:@", " intoString:NULL]; + XCTAssertNotEqual(northeastLatitude, 0); + double northeastLongitude; + [scan scanDouble:&northeastLongitude]; + XCTAssertNotEqual(northeastLongitude, 0); + + [scan scanString:@"),\nsouthwest: LatLng(" intoString:NULL]; + double southwestLatitude; + [scan scanDouble:&southwestLatitude]; + XCTAssertNotEqual(southwestLatitude, 0); + [scan scanString:@", " intoString:NULL]; + double southwestLongitude; + [scan scanDouble:&southwestLongitude]; + XCTAssertNotEqual(southwestLongitude, 0); + *northeast = CLLocationCoordinate2DMake(northeastLatitude, northeastLongitude); + *southwest = CLLocationCoordinate2DMake(southwestLatitude, southwestLongitude); } - (void)testMapClickPage { diff --git a/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerUITests/Info.plist similarity index 100% rename from packages/google_sign_in/google_sign_in/example/ios/RunnerTests/Info.plist rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerUITests/Info.plist diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/animate_camera.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/animate_camera.dart new file mode 100644 index 000000000000..c34a3ba4b2fe --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/animate_camera.dart @@ -0,0 +1,171 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class AnimateCameraPage extends GoogleMapExampleAppPage { + const AnimateCameraPage({Key? key}) + : super(const Icon(Icons.map), 'Camera control, animated', key: key); + + @override + Widget build(BuildContext context) { + return const AnimateCamera(); + } +} + +class AnimateCamera extends StatefulWidget { + const AnimateCamera({Key? key}) : super(key: key); + @override + State createState() => AnimateCameraState(); +} + +class AnimateCameraState extends State { + ExampleGoogleMapController? mapController; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + mapController = controller; + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: ExampleGoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: + const CameraPosition(target: LatLng(0.0, 0.0)), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.newCameraPosition( + const CameraPosition( + bearing: 270.0, + target: LatLng(51.5160895, -0.1294527), + tilt: 30.0, + zoom: 17.0, + ), + ), + ); + }, + child: const Text('newCameraPosition'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.newLatLng( + const LatLng(56.1725505, 10.1850512), + ), + ); + }, + child: const Text('newLatLng'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.newLatLngBounds( + LatLngBounds( + southwest: const LatLng(-38.483935, 113.248673), + northeast: const LatLng(-8.982446, 153.823821), + ), + 10.0, + ), + ); + }, + child: const Text('newLatLngBounds'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.newLatLngZoom( + const LatLng(37.4231613, -122.087159), + 11.0, + ), + ); + }, + child: const Text('newLatLngZoom'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.scrollBy(150.0, -225.0), + ); + }, + child: const Text('scrollBy'), + ), + ], + ), + Column( + children: [ + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.zoomBy( + -0.5, + const Offset(30.0, 20.0), + ), + ); + }, + child: const Text('zoomBy with focus'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.zoomBy(-0.5), + ); + }, + child: const Text('zoomBy'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.zoomIn(), + ); + }, + child: const Text('zoomIn'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.zoomOut(), + ); + }, + child: const Text('zoomOut'), + ), + TextButton( + onPressed: () { + mapController?.animateCamera( + CameraUpdate.zoomTo(16.0), + ); + }, + child: const Text('zoomTo'), + ), + ], + ), + ], + ) + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/example_google_map.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/example_google_map.dart new file mode 100644 index 000000000000..1c1261cb5b82 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/example_google_map.dart @@ -0,0 +1,538 @@ +// 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(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:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +// This is a pared down version of the Dart code from the app-facing package, +// to allow running the same examples for package-local testing. +// TODO(stuartmorgan): Consider extracting this to a shared package. See also +// https://github.com/flutter/flutter/issues/46716. + +/// Controller for a single ExampleGoogleMap instance running on the host platform. +class ExampleGoogleMapController { + ExampleGoogleMapController._( + this._googleMapState, { + required this.mapId, + }) { + _connectStreams(mapId); + } + + /// The mapId for this controller + final int mapId; + + /// Initialize control of a [ExampleGoogleMap] with [id]. + /// + /// Mainly for internal use when instantiating a [ExampleGoogleMapController] passed + /// in [ExampleGoogleMap.onMapCreated] callback. + static Future _init( + int id, + CameraPosition initialCameraPosition, + _ExampleGoogleMapState googleMapState, + ) async { + await GoogleMapsFlutterPlatform.instance.init(id); + return ExampleGoogleMapController._( + googleMapState, + mapId: id, + ); + } + + final _ExampleGoogleMapState _googleMapState; + + void _connectStreams(int mapId) { + if (_googleMapState.widget.onCameraMoveStarted != null) { + GoogleMapsFlutterPlatform.instance + .onCameraMoveStarted(mapId: mapId) + .listen((_) => _googleMapState.widget.onCameraMoveStarted!()); + } + if (_googleMapState.widget.onCameraMove != null) { + GoogleMapsFlutterPlatform.instance.onCameraMove(mapId: mapId).listen( + (CameraMoveEvent e) => _googleMapState.widget.onCameraMove!(e.value)); + } + if (_googleMapState.widget.onCameraIdle != null) { + GoogleMapsFlutterPlatform.instance + .onCameraIdle(mapId: mapId) + .listen((_) => _googleMapState.widget.onCameraIdle!()); + } + GoogleMapsFlutterPlatform.instance + .onMarkerTap(mapId: mapId) + .listen((MarkerTapEvent e) => _googleMapState.onMarkerTap(e.value)); + GoogleMapsFlutterPlatform.instance.onMarkerDragStart(mapId: mapId).listen( + (MarkerDragStartEvent e) => + _googleMapState.onMarkerDragStart(e.value, e.position)); + GoogleMapsFlutterPlatform.instance.onMarkerDrag(mapId: mapId).listen( + (MarkerDragEvent e) => + _googleMapState.onMarkerDrag(e.value, e.position)); + GoogleMapsFlutterPlatform.instance.onMarkerDragEnd(mapId: mapId).listen( + (MarkerDragEndEvent e) => + _googleMapState.onMarkerDragEnd(e.value, e.position)); + GoogleMapsFlutterPlatform.instance.onInfoWindowTap(mapId: mapId).listen( + (InfoWindowTapEvent e) => _googleMapState.onInfoWindowTap(e.value)); + GoogleMapsFlutterPlatform.instance + .onPolylineTap(mapId: mapId) + .listen((PolylineTapEvent e) => _googleMapState.onPolylineTap(e.value)); + GoogleMapsFlutterPlatform.instance + .onPolygonTap(mapId: mapId) + .listen((PolygonTapEvent e) => _googleMapState.onPolygonTap(e.value)); + GoogleMapsFlutterPlatform.instance + .onCircleTap(mapId: mapId) + .listen((CircleTapEvent e) => _googleMapState.onCircleTap(e.value)); + GoogleMapsFlutterPlatform.instance + .onTap(mapId: mapId) + .listen((MapTapEvent e) => _googleMapState.onTap(e.position)); + GoogleMapsFlutterPlatform.instance.onLongPress(mapId: mapId).listen( + (MapLongPressEvent e) => _googleMapState.onLongPress(e.position)); + } + + /// Updates configuration options of the map user interface. + Future _updateMapConfiguration(MapConfiguration update) { + return GoogleMapsFlutterPlatform.instance + .updateMapConfiguration(update, mapId: mapId); + } + + /// Updates marker configuration. + Future _updateMarkers(MarkerUpdates markerUpdates) { + return GoogleMapsFlutterPlatform.instance + .updateMarkers(markerUpdates, mapId: mapId); + } + + /// Updates polygon configuration. + Future _updatePolygons(PolygonUpdates polygonUpdates) { + return GoogleMapsFlutterPlatform.instance + .updatePolygons(polygonUpdates, mapId: mapId); + } + + /// Updates polyline configuration. + Future _updatePolylines(PolylineUpdates polylineUpdates) { + return GoogleMapsFlutterPlatform.instance + .updatePolylines(polylineUpdates, mapId: mapId); + } + + /// Updates circle configuration. + Future _updateCircles(CircleUpdates circleUpdates) { + return GoogleMapsFlutterPlatform.instance + .updateCircles(circleUpdates, mapId: mapId); + } + + /// Updates tile overlays configuration. + Future _updateTileOverlays(Set newTileOverlays) { + return GoogleMapsFlutterPlatform.instance + .updateTileOverlays(newTileOverlays: newTileOverlays, mapId: mapId); + } + + /// Clears the tile cache so that all tiles will be requested again from the + /// [TileProvider]. + Future clearTileCache(TileOverlayId tileOverlayId) async { + return GoogleMapsFlutterPlatform.instance + .clearTileCache(tileOverlayId, mapId: mapId); + } + + /// Starts an animated change of the map camera position. + Future animateCamera(CameraUpdate cameraUpdate) { + return GoogleMapsFlutterPlatform.instance + .animateCamera(cameraUpdate, mapId: mapId); + } + + /// Changes the map camera position. + Future moveCamera(CameraUpdate cameraUpdate) { + return GoogleMapsFlutterPlatform.instance + .moveCamera(cameraUpdate, mapId: mapId); + } + + /// Sets the styling of the base map. + Future setMapStyle(String? mapStyle) { + return GoogleMapsFlutterPlatform.instance + .setMapStyle(mapStyle, mapId: mapId); + } + + /// Return [LatLngBounds] defining the region that is visible in a map. + Future getVisibleRegion() { + return GoogleMapsFlutterPlatform.instance.getVisibleRegion(mapId: mapId); + } + + /// Return [ScreenCoordinate] of the [LatLng] in the current map view. + Future getScreenCoordinate(LatLng latLng) { + return GoogleMapsFlutterPlatform.instance + .getScreenCoordinate(latLng, mapId: mapId); + } + + /// Returns [LatLng] corresponding to the [ScreenCoordinate] in the current map view. + Future getLatLng(ScreenCoordinate screenCoordinate) { + return GoogleMapsFlutterPlatform.instance + .getLatLng(screenCoordinate, mapId: mapId); + } + + /// Programmatically show the Info Window for a [Marker]. + Future showMarkerInfoWindow(MarkerId markerId) { + return GoogleMapsFlutterPlatform.instance + .showMarkerInfoWindow(markerId, mapId: mapId); + } + + /// Programmatically hide the Info Window for a [Marker]. + Future hideMarkerInfoWindow(MarkerId markerId) { + return GoogleMapsFlutterPlatform.instance + .hideMarkerInfoWindow(markerId, mapId: mapId); + } + + /// Returns `true` when the [InfoWindow] is showing, `false` otherwise. + Future isMarkerInfoWindowShown(MarkerId markerId) { + return GoogleMapsFlutterPlatform.instance + .isMarkerInfoWindowShown(markerId, mapId: mapId); + } + + /// Returns the current zoom level of the map + Future getZoomLevel() { + return GoogleMapsFlutterPlatform.instance.getZoomLevel(mapId: mapId); + } + + /// Returns the image bytes of the map + Future takeSnapshot() { + return GoogleMapsFlutterPlatform.instance.takeSnapshot(mapId: mapId); + } + + /// Disposes of the platform resources + void dispose() { + GoogleMapsFlutterPlatform.instance.dispose(mapId: mapId); + } +} + +// The next map ID to create. +int _nextMapCreationId = 0; + +/// A widget which displays a map with data obtained from the Google Maps service. +class ExampleGoogleMap extends StatefulWidget { + /// Creates a widget displaying data from Google Maps services. + /// + /// [AssertionError] will be thrown if [initialCameraPosition] is null; + const ExampleGoogleMap({ + Key? key, + required this.initialCameraPosition, + this.onMapCreated, + this.gestureRecognizers = const >{}, + this.compassEnabled = true, + this.mapToolbarEnabled = true, + this.cameraTargetBounds = CameraTargetBounds.unbounded, + this.mapType = MapType.normal, + this.minMaxZoomPreference = MinMaxZoomPreference.unbounded, + this.rotateGesturesEnabled = true, + this.scrollGesturesEnabled = true, + this.zoomControlsEnabled = true, + this.zoomGesturesEnabled = true, + this.liteModeEnabled = false, + this.tiltGesturesEnabled = true, + this.myLocationEnabled = false, + this.myLocationButtonEnabled = true, + this.layoutDirection, + + /// If no padding is specified default padding will be 0. + this.padding = EdgeInsets.zero, + this.indoorViewEnabled = false, + this.trafficEnabled = false, + this.buildingsEnabled = true, + this.markers = const {}, + this.polygons = const {}, + this.polylines = const {}, + this.circles = const {}, + this.onCameraMoveStarted, + this.tileOverlays = const {}, + this.onCameraMove, + this.onCameraIdle, + this.onTap, + this.onLongPress, + }) : super(key: key); + + /// Callback method for when the map is ready to be used. + /// + /// Used to receive a [ExampleGoogleMapController] for this [ExampleGoogleMap]. + final void Function(ExampleGoogleMapController controller)? onMapCreated; + + /// The initial position of the map's camera. + final CameraPosition initialCameraPosition; + + /// True if the map should show a compass when rotated. + final bool compassEnabled; + + /// True if the map should show a toolbar when you interact with the map. Android only. + final bool mapToolbarEnabled; + + /// Geographical bounding box for the camera target. + final CameraTargetBounds cameraTargetBounds; + + /// Type of map tiles to be rendered. + final MapType mapType; + + /// The layout direction to use for the embedded view. + final TextDirection? layoutDirection; + + /// Preferred bounds for the camera zoom level. + /// + /// Actual bounds depend on map data and device. + final MinMaxZoomPreference minMaxZoomPreference; + + /// True if the map view should respond to rotate gestures. + final bool rotateGesturesEnabled; + + /// True if the map view should respond to scroll gestures. + final bool scrollGesturesEnabled; + + /// True if the map view should show zoom controls. This includes two buttons + /// to zoom in and zoom out. The default value is to show zoom controls. + final bool zoomControlsEnabled; + + /// True if the map view should respond to zoom gestures. + final bool zoomGesturesEnabled; + + /// True if the map view should be in lite mode. Android only. + final bool liteModeEnabled; + + /// True if the map view should respond to tilt gestures. + final bool tiltGesturesEnabled; + + /// Padding to be set on map. + final EdgeInsets padding; + + /// Markers to be placed on the map. + final Set markers; + + /// Polygons to be placed on the map. + final Set polygons; + + /// Polylines to be placed on the map. + final Set polylines; + + /// Circles to be placed on the map. + final Set circles; + + /// Tile overlays to be placed on the map. + final Set tileOverlays; + + /// Called when the camera starts moving. + final VoidCallback? onCameraMoveStarted; + + /// Called repeatedly as the camera continues to move after an + /// onCameraMoveStarted call. + final CameraPositionCallback? onCameraMove; + + /// Called when camera movement has ended, there are no pending + /// animations and the user has stopped interacting with the map. + final VoidCallback? onCameraIdle; + + /// Called every time a [ExampleGoogleMap] is tapped. + final ArgumentCallback? onTap; + + /// Called every time a [ExampleGoogleMap] is long pressed. + final ArgumentCallback? onLongPress; + + /// True if a "My Location" layer should be shown on the map. + final bool myLocationEnabled; + + /// Enables or disables the my-location button. + final bool myLocationButtonEnabled; + + /// Enables or disables the indoor view from the map + final bool indoorViewEnabled; + + /// Enables or disables the traffic layer of the map + final bool trafficEnabled; + + /// Enables or disables showing 3D buildings where available + final bool buildingsEnabled; + + /// Which gestures should be consumed by the map. + final Set> gestureRecognizers; + + /// Creates a [State] for this [ExampleGoogleMap]. + @override + State createState() => _ExampleGoogleMapState(); +} + +class _ExampleGoogleMapState extends State { + final int _mapId = _nextMapCreationId++; + + final Completer _controller = + Completer(); + + Map _markers = {}; + Map _polygons = {}; + Map _polylines = {}; + Map _circles = {}; + late MapConfiguration _mapConfiguration; + + @override + Widget build(BuildContext context) { + return GoogleMapsFlutterPlatform.instance.buildViewWithConfiguration( + _mapId, + onPlatformViewCreated, + widgetConfiguration: MapWidgetConfiguration( + textDirection: widget.layoutDirection ?? + Directionality.maybeOf(context) ?? + TextDirection.ltr, + initialCameraPosition: widget.initialCameraPosition, + gestureRecognizers: widget.gestureRecognizers, + ), + mapObjects: MapObjects( + markers: widget.markers, + polygons: widget.polygons, + polylines: widget.polylines, + circles: widget.circles, + ), + mapConfiguration: _mapConfiguration, + ); + } + + @override + void initState() { + super.initState(); + _mapConfiguration = _configurationFromMapWidget(widget); + _markers = keyByMarkerId(widget.markers); + _polygons = keyByPolygonId(widget.polygons); + _polylines = keyByPolylineId(widget.polylines); + _circles = keyByCircleId(widget.circles); + } + + @override + void dispose() { + _controller.future + .then((ExampleGoogleMapController controller) => controller.dispose()); + super.dispose(); + } + + @override + void didUpdateWidget(ExampleGoogleMap oldWidget) { + super.didUpdateWidget(oldWidget); + _updateOptions(); + _updateMarkers(); + _updatePolygons(); + _updatePolylines(); + _updateCircles(); + _updateTileOverlays(); + } + + Future _updateOptions() async { + final MapConfiguration newConfig = _configurationFromMapWidget(widget); + final MapConfiguration updates = newConfig.diffFrom(_mapConfiguration); + if (updates.isEmpty) { + return; + } + final ExampleGoogleMapController controller = await _controller.future; + controller._updateMapConfiguration(updates); + _mapConfiguration = newConfig; + } + + Future _updateMarkers() async { + final ExampleGoogleMapController controller = await _controller.future; + controller._updateMarkers( + MarkerUpdates.from(_markers.values.toSet(), widget.markers)); + _markers = keyByMarkerId(widget.markers); + } + + Future _updatePolygons() async { + final ExampleGoogleMapController controller = await _controller.future; + controller._updatePolygons( + PolygonUpdates.from(_polygons.values.toSet(), widget.polygons)); + _polygons = keyByPolygonId(widget.polygons); + } + + Future _updatePolylines() async { + final ExampleGoogleMapController controller = await _controller.future; + controller._updatePolylines( + PolylineUpdates.from(_polylines.values.toSet(), widget.polylines)); + _polylines = keyByPolylineId(widget.polylines); + } + + Future _updateCircles() async { + final ExampleGoogleMapController controller = await _controller.future; + controller._updateCircles( + CircleUpdates.from(_circles.values.toSet(), widget.circles)); + _circles = keyByCircleId(widget.circles); + } + + Future _updateTileOverlays() async { + final ExampleGoogleMapController controller = await _controller.future; + controller._updateTileOverlays(widget.tileOverlays); + } + + Future onPlatformViewCreated(int id) async { + final ExampleGoogleMapController controller = + await ExampleGoogleMapController._init( + id, + widget.initialCameraPosition, + this, + ); + _controller.complete(controller); + _updateTileOverlays(); + widget.onMapCreated?.call(controller); + } + + void onMarkerTap(MarkerId markerId) { + _markers[markerId]!.onTap?.call(); + } + + void onMarkerDragStart(MarkerId markerId, LatLng position) { + _markers[markerId]!.onDragStart?.call(position); + } + + void onMarkerDrag(MarkerId markerId, LatLng position) { + _markers[markerId]!.onDrag?.call(position); + } + + void onMarkerDragEnd(MarkerId markerId, LatLng position) { + _markers[markerId]!.onDragEnd?.call(position); + } + + void onPolygonTap(PolygonId polygonId) { + _polygons[polygonId]!.onTap?.call(); + } + + void onPolylineTap(PolylineId polylineId) { + _polylines[polylineId]!.onTap?.call(); + } + + void onCircleTap(CircleId circleId) { + _circles[circleId]!.onTap?.call(); + } + + void onInfoWindowTap(MarkerId markerId) { + _markers[markerId]!.infoWindow.onTap?.call(); + } + + void onTap(LatLng position) { + widget.onTap?.call(position); + } + + void onLongPress(LatLng position) { + widget.onLongPress?.call(position); + } +} + +/// Builds a [MapConfiguration] from the given [map]. +MapConfiguration _configurationFromMapWidget(ExampleGoogleMap map) { + return MapConfiguration( + compassEnabled: map.compassEnabled, + mapToolbarEnabled: map.mapToolbarEnabled, + cameraTargetBounds: map.cameraTargetBounds, + mapType: map.mapType, + minMaxZoomPreference: map.minMaxZoomPreference, + rotateGesturesEnabled: map.rotateGesturesEnabled, + scrollGesturesEnabled: map.scrollGesturesEnabled, + tiltGesturesEnabled: map.tiltGesturesEnabled, + trackCameraPosition: map.onCameraMove != null, + zoomControlsEnabled: map.zoomControlsEnabled, + zoomGesturesEnabled: map.zoomGesturesEnabled, + liteModeEnabled: map.liteModeEnabled, + myLocationEnabled: map.myLocationEnabled, + myLocationButtonEnabled: map.myLocationButtonEnabled, + padding: map.padding, + indoorViewEnabled: map.indoorViewEnabled, + trafficEnabled: map.trafficEnabled, + buildingsEnabled: map.buildingsEnabled, + ); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/lite_mode.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/lite_mode.dart new file mode 100644 index 000000000000..f7bead951f5d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/lite_mode.dart @@ -0,0 +1,47 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +const CameraPosition _kInitialPosition = + CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); + +class LiteModePage extends GoogleMapExampleAppPage { + const LiteModePage({Key? key}) + : super(const Icon(Icons.map), 'Lite mode', key: key); + + @override + Widget build(BuildContext context) { + return const _LiteModeBody(); + } +} + +class _LiteModeBody extends StatelessWidget { + const _LiteModeBody(); + + @override + Widget build(BuildContext context) { + return const Card( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 30.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: _kInitialPosition, + liteModeEnabled: true, + ), + ), + ), + ), + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/main.dart new file mode 100644 index 000000000000..de75162b09dd --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/main.dart @@ -0,0 +1,73 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'animate_camera.dart'; +import 'lite_mode.dart'; +import 'map_click.dart'; +import 'map_coordinates.dart'; +import 'map_ui.dart'; +import 'marker_icons.dart'; +import 'move_camera.dart'; +import 'padding.dart'; +import 'page.dart'; +import 'place_circle.dart'; +import 'place_marker.dart'; +import 'place_polygon.dart'; +import 'place_polyline.dart'; +import 'scrolling_map.dart'; +import 'snapshot.dart'; +import 'tile_overlay.dart'; + +final List _allPages = [ + const MapUiPage(), + const MapCoordinatesPage(), + const MapClickPage(), + const AnimateCameraPage(), + const MoveCameraPage(), + const PlaceMarkerPage(), + const MarkerIconsPage(), + const ScrollingMapPage(), + const PlacePolylinePage(), + const PlacePolygonPage(), + const PlaceCirclePage(), + const PaddingPage(), + const SnapshotPage(), + const LiteModePage(), + const TileOverlayPage(), +]; + +/// MapsDemo is the Main Application. +class MapsDemo extends StatelessWidget { + /// Default Constructor + const MapsDemo({Key? key}) : super(key: key); + + void _pushPage(BuildContext context, GoogleMapExampleAppPage page) { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => Scaffold( + appBar: AppBar(title: Text(page.title)), + body: page, + ))); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('GoogleMaps examples')), + body: ListView.builder( + itemCount: _allPages.length, + itemBuilder: (_, int index) => ListTile( + leading: _allPages[index].leading, + title: Text(_allPages[index].title), + onTap: () => _pushPage(context, _allPages[index]), + ), + ), + ); + } +} + +void main() { + runApp(const MaterialApp(home: MapsDemo())); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_click.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_click.dart new file mode 100644 index 000000000000..4017a9fccce2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_click.dart @@ -0,0 +1,105 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +const CameraPosition _kInitialPosition = + CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); + +class MapClickPage extends GoogleMapExampleAppPage { + const MapClickPage({Key? key}) + : super(const Icon(Icons.mouse), 'Map click', key: key); + + @override + Widget build(BuildContext context) { + return const _MapClickBody(); + } +} + +class _MapClickBody extends StatefulWidget { + const _MapClickBody(); + + @override + State createState() => _MapClickBodyState(); +} + +class _MapClickBodyState extends State<_MapClickBody> { + _MapClickBodyState(); + + ExampleGoogleMapController? mapController; + LatLng? _lastTap; + LatLng? _lastLongPress; + + @override + Widget build(BuildContext context) { + final ExampleGoogleMap googleMap = ExampleGoogleMap( + onMapCreated: onMapCreated, + initialCameraPosition: _kInitialPosition, + onTap: (LatLng pos) { + setState(() { + _lastTap = pos; + }); + }, + onLongPress: (LatLng pos) { + setState(() { + _lastLongPress = pos; + }); + }, + ); + + final List columnChildren = [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: googleMap, + ), + ), + ), + ]; + + if (mapController != null) { + final String lastTap = 'Tap:\n${_lastTap ?? ""}\n'; + final String lastLongPress = 'Long press:\n${_lastLongPress ?? ""}'; + columnChildren.add(Center( + child: Text( + lastTap, + textAlign: TextAlign.center, + ))); + columnChildren.add(Center( + child: Text( + _lastTap != null ? 'Tapped' : '', + textAlign: TextAlign.center, + ))); + columnChildren.add(Center( + child: Text( + lastLongPress, + textAlign: TextAlign.center, + ))); + columnChildren.add(Center( + child: Text( + _lastLongPress != null ? 'Long pressed' : '', + textAlign: TextAlign.center, + ))); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: columnChildren, + ); + } + + Future onMapCreated(ExampleGoogleMapController controller) async { + setState(() { + mapController = controller; + }); + } +} 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 new file mode 100644 index 000000000000..185a97e08f00 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_coordinates.dart @@ -0,0 +1,100 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +const CameraPosition _kInitialPosition = + CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); + +class MapCoordinatesPage extends GoogleMapExampleAppPage { + const MapCoordinatesPage({Key? key}) + : super(const Icon(Icons.map), 'Map coordinates', key: key); + + @override + Widget build(BuildContext context) { + return const _MapCoordinatesBody(); + } +} + +class _MapCoordinatesBody extends StatefulWidget { + const _MapCoordinatesBody(); + + @override + State createState() => _MapCoordinatesBodyState(); +} + +class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { + _MapCoordinatesBodyState(); + + ExampleGoogleMapController? mapController; + LatLngBounds _visibleRegion = LatLngBounds( + southwest: const LatLng(0, 0), + northeast: const LatLng(0, 0), + ); + + @override + Widget build(BuildContext context) { + final ExampleGoogleMap googleMap = ExampleGoogleMap( + onMapCreated: onMapCreated, + initialCameraPosition: _kInitialPosition, + onCameraIdle: + _updateVisibleRegion, // https://github.com/flutter/flutter/issues/54758 + ); + + return NotificationListener( + onNotification: (ScrollNotification scrollState) { + _updateVisibleRegion(); + return true; + }, + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: googleMap, + ), + ), + ), + if (mapController != null) + Center( + child: Text('VisibleRegion:' + '\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 + Container( + width: 300, + height: 1000, + ), + ], + ), + ); + } + + Future onMapCreated(ExampleGoogleMapController controller) async { + final LatLngBounds visibleRegion = await controller.getVisibleRegion(); + setState(() { + mapController = controller; + _visibleRegion = visibleRegion; + }); + } + + Future _updateVisibleRegion() async { + final LatLngBounds visibleRegion = await mapController!.getVisibleRegion(); + setState(() { + _visibleRegion = visibleRegion; + }); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_ui.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_ui.dart new file mode 100644 index 000000000000..009ee71d8400 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_ui.dart @@ -0,0 +1,357 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +final LatLngBounds sydneyBounds = LatLngBounds( + southwest: const LatLng(-34.022631, 150.620685), + northeast: const LatLng(-33.571835, 151.325952), +); + +class MapUiPage extends GoogleMapExampleAppPage { + const MapUiPage({Key? key}) + : super(const Icon(Icons.map), 'User interface', key: key); + + @override + Widget build(BuildContext context) { + return const MapUiBody(); + } +} + +class MapUiBody extends StatefulWidget { + const MapUiBody({Key? key}) : super(key: key); + + @override + State createState() => MapUiBodyState(); +} + +class MapUiBodyState extends State { + MapUiBodyState(); + + static const CameraPosition _kInitialPosition = CameraPosition( + target: LatLng(-33.852, 151.211), + zoom: 11.0, + ); + + CameraPosition _position = _kInitialPosition; + bool _isMapCreated = false; + final bool _isMoving = false; + bool _compassEnabled = true; + bool _mapToolbarEnabled = true; + CameraTargetBounds _cameraTargetBounds = CameraTargetBounds.unbounded; + MinMaxZoomPreference _minMaxZoomPreference = MinMaxZoomPreference.unbounded; + MapType _mapType = MapType.normal; + bool _rotateGesturesEnabled = true; + bool _scrollGesturesEnabled = true; + bool _tiltGesturesEnabled = true; + bool _zoomControlsEnabled = false; + bool _zoomGesturesEnabled = true; + bool _indoorViewEnabled = true; + bool _myLocationEnabled = true; + bool _myTrafficEnabled = false; + bool _myLocationButtonEnabled = true; + late ExampleGoogleMapController _controller; + bool _nightMode = false; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + Widget _compassToggler() { + return TextButton( + child: Text('${_compassEnabled ? 'disable' : 'enable'} compass'), + onPressed: () { + setState(() { + _compassEnabled = !_compassEnabled; + }); + }, + ); + } + + Widget _mapToolbarToggler() { + return TextButton( + child: Text('${_mapToolbarEnabled ? 'disable' : 'enable'} map toolbar'), + onPressed: () { + setState(() { + _mapToolbarEnabled = !_mapToolbarEnabled; + }); + }, + ); + } + + Widget _latLngBoundsToggler() { + return TextButton( + child: Text( + _cameraTargetBounds.bounds == null + ? 'bound camera target' + : 'release camera target', + ), + onPressed: () { + setState(() { + _cameraTargetBounds = _cameraTargetBounds.bounds == null + ? CameraTargetBounds(sydneyBounds) + : CameraTargetBounds.unbounded; + }); + }, + ); + } + + Widget _zoomBoundsToggler() { + return TextButton( + child: Text(_minMaxZoomPreference.minZoom == null + ? 'bound zoom' + : 'release zoom'), + onPressed: () { + setState(() { + _minMaxZoomPreference = _minMaxZoomPreference.minZoom == null + ? const MinMaxZoomPreference(12.0, 16.0) + : MinMaxZoomPreference.unbounded; + }); + }, + ); + } + + Widget _mapTypeCycler() { + final MapType nextType = + MapType.values[(_mapType.index + 1) % MapType.values.length]; + return TextButton( + child: Text('change map type to $nextType'), + onPressed: () { + setState(() { + _mapType = nextType; + }); + }, + ); + } + + Widget _rotateToggler() { + return TextButton( + child: Text('${_rotateGesturesEnabled ? 'disable' : 'enable'} rotate'), + onPressed: () { + setState(() { + _rotateGesturesEnabled = !_rotateGesturesEnabled; + }); + }, + ); + } + + Widget _scrollToggler() { + return TextButton( + child: Text('${_scrollGesturesEnabled ? 'disable' : 'enable'} scroll'), + onPressed: () { + setState(() { + _scrollGesturesEnabled = !_scrollGesturesEnabled; + }); + }, + ); + } + + Widget _tiltToggler() { + return TextButton( + child: Text('${_tiltGesturesEnabled ? 'disable' : 'enable'} tilt'), + onPressed: () { + setState(() { + _tiltGesturesEnabled = !_tiltGesturesEnabled; + }); + }, + ); + } + + Widget _zoomToggler() { + return TextButton( + child: Text('${_zoomGesturesEnabled ? 'disable' : 'enable'} zoom'), + onPressed: () { + setState(() { + _zoomGesturesEnabled = !_zoomGesturesEnabled; + }); + }, + ); + } + + Widget _zoomControlsToggler() { + return TextButton( + child: + Text('${_zoomControlsEnabled ? 'disable' : 'enable'} zoom controls'), + onPressed: () { + setState(() { + _zoomControlsEnabled = !_zoomControlsEnabled; + }); + }, + ); + } + + Widget _indoorViewToggler() { + return TextButton( + child: Text('${_indoorViewEnabled ? 'disable' : 'enable'} indoor'), + onPressed: () { + setState(() { + _indoorViewEnabled = !_indoorViewEnabled; + }); + }, + ); + } + + Widget _myLocationToggler() { + return TextButton( + child: Text( + '${_myLocationEnabled ? 'disable' : 'enable'} my location marker'), + onPressed: () { + setState(() { + _myLocationEnabled = !_myLocationEnabled; + }); + }, + ); + } + + Widget _myLocationButtonToggler() { + return TextButton( + child: Text( + '${_myLocationButtonEnabled ? 'disable' : 'enable'} my location button'), + onPressed: () { + setState(() { + _myLocationButtonEnabled = !_myLocationButtonEnabled; + }); + }, + ); + } + + Widget _myTrafficToggler() { + return TextButton( + child: Text('${_myTrafficEnabled ? 'disable' : 'enable'} my traffic'), + onPressed: () { + setState(() { + _myTrafficEnabled = !_myTrafficEnabled; + }); + }, + ); + } + + Future _getFileData(String path) async { + return await rootBundle.loadString(path); + } + + void _setMapStyle(String mapStyle) { + setState(() { + _nightMode = true; + _controller.setMapStyle(mapStyle); + }); + } + + // Should only be called if _isMapCreated is true. + Widget _nightModeToggler() { + assert(_isMapCreated); + return TextButton( + child: Text('${_nightMode ? 'disable' : 'enable'} night mode'), + onPressed: () { + if (_nightMode) { + setState(() { + _nightMode = false; + _controller.setMapStyle(null); + }); + } else { + _getFileData('assets/night_mode.json').then(_setMapStyle); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + final ExampleGoogleMap googleMap = ExampleGoogleMap( + onMapCreated: onMapCreated, + initialCameraPosition: _kInitialPosition, + compassEnabled: _compassEnabled, + mapToolbarEnabled: _mapToolbarEnabled, + cameraTargetBounds: _cameraTargetBounds, + minMaxZoomPreference: _minMaxZoomPreference, + mapType: _mapType, + rotateGesturesEnabled: _rotateGesturesEnabled, + scrollGesturesEnabled: _scrollGesturesEnabled, + tiltGesturesEnabled: _tiltGesturesEnabled, + zoomGesturesEnabled: _zoomGesturesEnabled, + zoomControlsEnabled: _zoomControlsEnabled, + indoorViewEnabled: _indoorViewEnabled, + myLocationEnabled: _myLocationEnabled, + myLocationButtonEnabled: _myLocationButtonEnabled, + trafficEnabled: _myTrafficEnabled, + onCameraMove: _updateCameraPosition, + ); + + final List columnChildren = [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: googleMap, + ), + ), + ), + ]; + + if (_isMapCreated) { + columnChildren.add( + Expanded( + child: ListView( + children: [ + Text('camera bearing: ${_position.bearing}'), + Text( + 'camera target: ${_position.target.latitude.toStringAsFixed(4)},' + '${_position.target.longitude.toStringAsFixed(4)}'), + Text('camera zoom: ${_position.zoom}'), + Text('camera tilt: ${_position.tilt}'), + Text(_isMoving ? '(Camera moving)' : '(Camera idle)'), + _compassToggler(), + _mapToolbarToggler(), + _latLngBoundsToggler(), + _mapTypeCycler(), + _zoomBoundsToggler(), + _rotateToggler(), + _scrollToggler(), + _tiltToggler(), + _zoomToggler(), + _zoomControlsToggler(), + _indoorViewToggler(), + _myLocationToggler(), + _myLocationButtonToggler(), + _myTrafficToggler(), + _nightModeToggler(), + ], + ), + ), + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: columnChildren, + ); + } + + void _updateCameraPosition(CameraPosition position) { + setState(() { + _position = position; + }); + } + + void onMapCreated(ExampleGoogleMapController controller) { + setState(() { + _controller = controller; + _isMapCreated = true; + }); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/marker_icons.dart new file mode 100644 index 000000000000..fe28eb680596 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/marker_icons.dart @@ -0,0 +1,98 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs +// ignore_for_file: unawaited_futures + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class MarkerIconsPage extends GoogleMapExampleAppPage { + const MarkerIconsPage({Key? key}) + : super(const Icon(Icons.image), 'Marker icons', key: key); + + @override + Widget build(BuildContext context) { + return const MarkerIconsBody(); + } +} + +class MarkerIconsBody extends StatefulWidget { + const MarkerIconsBody({Key? key}) : super(key: key); + + @override + State createState() => MarkerIconsBodyState(); +} + +const LatLng _kMapCenter = LatLng(52.4478, -3.5402); + +class MarkerIconsBodyState extends State { + ExampleGoogleMapController? controller; + BitmapDescriptor? _markerIcon; + + @override + Widget build(BuildContext context) { + _createMarkerImageFromAsset(context); + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: _kMapCenter, + zoom: 7.0, + ), + markers: {_createMarker()}, + onMapCreated: _onMapCreated, + ), + ), + ) + ], + ); + } + + Marker _createMarker() { + if (_markerIcon != null) { + return Marker( + markerId: const MarkerId('marker_1'), + position: _kMapCenter, + icon: _markerIcon!, + ); + } else { + return const Marker( + markerId: MarkerId('marker_1'), + position: _kMapCenter, + ); + } + } + + Future _createMarkerImageFromAsset(BuildContext context) async { + if (_markerIcon == null) { + final ImageConfiguration imageConfiguration = + createLocalImageConfiguration(context, size: const Size.square(48)); + BitmapDescriptor.fromAssetImage( + imageConfiguration, 'assets/red_square.png') + .then(_updateBitmap); + } + } + + void _updateBitmap(BitmapDescriptor bitmap) { + setState(() { + _markerIcon = bitmap; + }); + } + + void _onMapCreated(ExampleGoogleMapController controllerParam) { + setState(() { + controller = controllerParam; + }); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/move_camera.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/move_camera.dart new file mode 100644 index 000000000000..7f44d89518dc --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/move_camera.dart @@ -0,0 +1,171 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class MoveCameraPage extends GoogleMapExampleAppPage { + const MoveCameraPage({Key? key}) + : super(const Icon(Icons.map), 'Camera control', key: key); + + @override + Widget build(BuildContext context) { + return const MoveCamera(); + } +} + +class MoveCamera extends StatefulWidget { + const MoveCamera({Key? key}) : super(key: key); + @override + State createState() => MoveCameraState(); +} + +class MoveCameraState extends State { + ExampleGoogleMapController? mapController; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + mapController = controller; + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: ExampleGoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: + const CameraPosition(target: LatLng(0.0, 0.0)), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.newCameraPosition( + const CameraPosition( + bearing: 270.0, + target: LatLng(51.5160895, -0.1294527), + tilt: 30.0, + zoom: 17.0, + ), + ), + ); + }, + child: const Text('newCameraPosition'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.newLatLng( + const LatLng(56.1725505, 10.1850512), + ), + ); + }, + child: const Text('newLatLng'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.newLatLngBounds( + LatLngBounds( + southwest: const LatLng(-38.483935, 113.248673), + northeast: const LatLng(-8.982446, 153.823821), + ), + 10.0, + ), + ); + }, + child: const Text('newLatLngBounds'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.newLatLngZoom( + const LatLng(37.4231613, -122.087159), + 11.0, + ), + ); + }, + child: const Text('newLatLngZoom'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.scrollBy(150.0, -225.0), + ); + }, + child: const Text('scrollBy'), + ), + ], + ), + Column( + children: [ + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.zoomBy( + -0.5, + const Offset(30.0, 20.0), + ), + ); + }, + child: const Text('zoomBy with focus'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.zoomBy(-0.5), + ); + }, + child: const Text('zoomBy'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.zoomIn(), + ); + }, + child: const Text('zoomIn'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.zoomOut(), + ); + }, + child: const Text('zoomOut'), + ), + TextButton( + onPressed: () { + mapController?.moveCamera( + CameraUpdate.zoomTo(16.0), + ); + }, + child: const Text('zoomTo'), + ), + ], + ), + ], + ) + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/padding.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/padding.dart new file mode 100644 index 000000000000..98be700a2af2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/padding.dart @@ -0,0 +1,180 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class PaddingPage extends GoogleMapExampleAppPage { + const PaddingPage({Key? key}) + : super(const Icon(Icons.map), 'Add padding to the map', key: key); + + @override + Widget build(BuildContext context) { + return const MarkerIconsBody(); + } +} + +class MarkerIconsBody extends StatefulWidget { + const MarkerIconsBody({Key? key}) : super(key: key); + + @override + State createState() => MarkerIconsBodyState(); +} + +const LatLng _kMapCenter = LatLng(52.4478, -3.5402); + +class MarkerIconsBodyState extends State { + ExampleGoogleMapController? controller; + + EdgeInsets _padding = EdgeInsets.zero; + + @override + Widget build(BuildContext context) { + final ExampleGoogleMap googleMap = ExampleGoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition( + target: _kMapCenter, + zoom: 7.0, + ), + padding: _padding, + ); + + final List columnChildren = [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: googleMap, + ), + ), + ), + const Padding( + padding: EdgeInsets.only(top: 20), + child: Center( + child: Text( + 'Enter Padding Below', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + ), + ]; + + columnChildren.addAll([_paddingInput(), _buttons()]); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: columnChildren, + ); + } + + void _onMapCreated(ExampleGoogleMapController controllerParam) { + setState(() { + controller = controllerParam; + }); + } + + final TextEditingController _topController = TextEditingController(); + final TextEditingController _bottomController = TextEditingController(); + final TextEditingController _leftController = TextEditingController(); + final TextEditingController _rightController = TextEditingController(); + + Widget _paddingInput() { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Flexible( + flex: 2, + child: TextField( + controller: _topController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + decoration: const InputDecoration( + hintText: 'Top', + ), + ), + ), + const Spacer(), + Flexible( + flex: 2, + child: TextField( + controller: _bottomController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + decoration: const InputDecoration( + hintText: 'Bottom', + ), + ), + ), + const Spacer(), + Flexible( + flex: 2, + child: TextField( + controller: _leftController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + decoration: const InputDecoration( + hintText: 'Left', + ), + ), + ), + const Spacer(), + Flexible( + flex: 2, + child: TextField( + controller: _rightController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + decoration: const InputDecoration( + hintText: 'Right', + ), + ), + ), + ], + ), + ); + } + + Widget _buttons() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + child: const Text('Set Padding'), + onPressed: () { + setState(() { + _padding = EdgeInsets.fromLTRB( + double.tryParse(_leftController.value.text) ?? 0, + double.tryParse(_topController.value.text) ?? 0, + double.tryParse(_rightController.value.text) ?? 0, + double.tryParse(_bottomController.value.text) ?? 0); + }); + }, + ), + TextButton( + child: const Text('Reset Padding'), + onPressed: () { + setState(() { + _topController.clear(); + _bottomController.clear(); + _leftController.clear(); + _rightController.clear(); + _padding = EdgeInsets.zero; + }); + }, + ) + ], + ), + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/page.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/page.dart new file mode 100644 index 000000000000..eb01ab07a6f3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/page.dart @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; + +abstract class GoogleMapExampleAppPage extends StatelessWidget { + const GoogleMapExampleAppPage(this.leading, this.title, {Key? key}) + : super(key: key); + + final Widget leading; + final String title; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_circle.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_circle.dart new file mode 100644 index 000000000000..9dc5760afa1f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_circle.dart @@ -0,0 +1,232 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class PlaceCirclePage extends GoogleMapExampleAppPage { + const PlaceCirclePage({Key? key}) + : super(const Icon(Icons.linear_scale), 'Place circle', key: key); + + @override + Widget build(BuildContext context) { + return const PlaceCircleBody(); + } +} + +class PlaceCircleBody extends StatefulWidget { + const PlaceCircleBody({Key? key}) : super(key: key); + + @override + State createState() => PlaceCircleBodyState(); +} + +class PlaceCircleBodyState extends State { + PlaceCircleBodyState(); + + ExampleGoogleMapController? controller; + Map circles = {}; + int _circleIdCounter = 1; + CircleId? selectedCircle; + + // Values when toggling circle color + int fillColorsIndex = 0; + int strokeColorsIndex = 0; + List colors = [ + Colors.purple, + Colors.red, + Colors.green, + Colors.pink, + ]; + + // Values when toggling circle stroke width + int widthsIndex = 0; + List widths = [10, 20, 5]; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _onCircleTapped(CircleId circleId) { + setState(() { + selectedCircle = circleId; + }); + } + + void _remove(CircleId circleId) { + setState(() { + if (circles.containsKey(circleId)) { + circles.remove(circleId); + } + if (circleId == selectedCircle) { + selectedCircle = null; + } + }); + } + + void _add() { + final int circleCount = circles.length; + + if (circleCount == 12) { + return; + } + + final String circleIdVal = 'circle_id_$_circleIdCounter'; + _circleIdCounter++; + final CircleId circleId = CircleId(circleIdVal); + + final Circle circle = Circle( + circleId: circleId, + consumeTapEvents: true, + strokeColor: Colors.orange, + fillColor: Colors.green, + strokeWidth: 5, + center: _createCenter(), + radius: 50000, + onTap: () { + _onCircleTapped(circleId); + }, + ); + + setState(() { + circles[circleId] = circle; + }); + } + + void _toggleVisible(CircleId circleId) { + final Circle circle = circles[circleId]!; + setState(() { + circles[circleId] = circle.copyWith( + visibleParam: !circle.visible, + ); + }); + } + + void _changeFillColor(CircleId circleId) { + final Circle circle = circles[circleId]!; + setState(() { + circles[circleId] = circle.copyWith( + fillColorParam: colors[++fillColorsIndex % colors.length], + ); + }); + } + + void _changeStrokeColor(CircleId circleId) { + final Circle circle = circles[circleId]!; + setState(() { + circles[circleId] = circle.copyWith( + strokeColorParam: colors[++strokeColorsIndex % colors.length], + ); + }); + } + + void _changeStrokeWidth(CircleId circleId) { + final Circle circle = circles[circleId]!; + setState(() { + circles[circleId] = circle.copyWith( + strokeWidthParam: widths[++widthsIndex % widths.length], + ); + }); + } + + @override + Widget build(BuildContext context) { + final CircleId? selectedId = selectedCircle; + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: LatLng(52.4478, -3.5402), + zoom: 7.0, + ), + circles: Set.of(circles.values), + onMapCreated: _onMapCreated, + ), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Column( + children: [ + TextButton( + onPressed: _add, + child: const Text('add'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _remove(selectedId), + child: const Text('remove'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _toggleVisible(selectedId), + child: const Text('toggle visible'), + ), + ], + ), + Column( + children: [ + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeStrokeWidth(selectedId), + child: const Text('change stroke width'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeStrokeColor(selectedId), + child: const Text('change stroke color'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeFillColor(selectedId), + child: const Text('change fill color'), + ), + ], + ) + ], + ) + ], + ), + ), + ), + ], + ); + } + + LatLng _createCenter() { + final double offset = _circleIdCounter.ceilToDouble(); + return _createLatLng(51.4816 + offset * 0.2, -3.1791); + } + + LatLng _createLatLng(double lat, double lng) { + return LatLng(lat, lng); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_marker.dart new file mode 100644 index 000000000000..7d12f4c81684 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_marker.dart @@ -0,0 +1,421 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:math'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class PlaceMarkerPage extends GoogleMapExampleAppPage { + const PlaceMarkerPage({Key? key}) + : super(const Icon(Icons.place), 'Place marker', key: key); + + @override + Widget build(BuildContext context) { + return const PlaceMarkerBody(); + } +} + +class PlaceMarkerBody extends StatefulWidget { + const PlaceMarkerBody({Key? key}) : super(key: key); + + @override + State createState() => PlaceMarkerBodyState(); +} + +typedef MarkerUpdateAction = Marker Function(Marker marker); + +class PlaceMarkerBodyState extends State { + PlaceMarkerBodyState(); + static const LatLng center = LatLng(-33.86711, 151.1947171); + + ExampleGoogleMapController? controller; + Map markers = {}; + MarkerId? selectedMarker; + int _markerIdCounter = 1; + LatLng? markerPosition; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _onMarkerTapped(MarkerId markerId) { + final Marker? tappedMarker = markers[markerId]; + if (tappedMarker != null) { + setState(() { + final MarkerId? previousMarkerId = selectedMarker; + if (previousMarkerId != null && markers.containsKey(previousMarkerId)) { + final Marker resetOld = markers[previousMarkerId]! + .copyWith(iconParam: BitmapDescriptor.defaultMarker); + markers[previousMarkerId] = resetOld; + } + selectedMarker = markerId; + final Marker newMarker = tappedMarker.copyWith( + iconParam: BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueGreen, + ), + ); + markers[markerId] = newMarker; + + markerPosition = null; + }); + } + } + + Future _onMarkerDrag(MarkerId markerId, LatLng newPosition) async { + setState(() { + markerPosition = newPosition; + }); + } + + Future _onMarkerDragEnd(MarkerId markerId, LatLng newPosition) async { + final Marker? tappedMarker = markers[markerId]; + if (tappedMarker != null) { + setState(() { + markerPosition = null; + }); + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () => Navigator.of(context).pop(), + ) + ], + content: Padding( + padding: const EdgeInsets.symmetric(vertical: 66), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Old position: ${tappedMarker.position}'), + Text('New position: $newPosition'), + ], + ))); + }); + } + } + + void _add() { + final int markerCount = markers.length; + + if (markerCount == 12) { + return; + } + + final String markerIdVal = 'marker_id_$_markerIdCounter'; + _markerIdCounter++; + final MarkerId markerId = MarkerId(markerIdVal); + + final Marker marker = Marker( + markerId: markerId, + position: LatLng( + center.latitude + sin(_markerIdCounter * pi / 6.0) / 20.0, + center.longitude + cos(_markerIdCounter * pi / 6.0) / 20.0, + ), + infoWindow: InfoWindow(title: markerIdVal, snippet: '*'), + onTap: () => _onMarkerTapped(markerId), + onDragEnd: (LatLng position) => _onMarkerDragEnd(markerId, position), + onDrag: (LatLng position) => _onMarkerDrag(markerId, position), + ); + + setState(() { + markers[markerId] = marker; + }); + } + + void _remove(MarkerId markerId) { + setState(() { + if (markers.containsKey(markerId)) { + markers.remove(markerId); + } + }); + } + + void _changePosition(MarkerId markerId) { + final Marker marker = markers[markerId]!; + final LatLng current = marker.position; + final Offset offset = Offset( + center.latitude - current.latitude, + center.longitude - current.longitude, + ); + setState(() { + markers[markerId] = marker.copyWith( + positionParam: LatLng( + center.latitude + offset.dy, + center.longitude + offset.dx, + ), + ); + }); + } + + void _changeAnchor(MarkerId markerId) { + final Marker marker = markers[markerId]!; + final Offset currentAnchor = marker.anchor; + final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx); + setState(() { + markers[markerId] = marker.copyWith( + anchorParam: newAnchor, + ); + }); + } + + Future _changeInfoAnchor(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + final Offset currentAnchor = marker.infoWindow.anchor; + final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx); + setState(() { + markers[markerId] = marker.copyWith( + infoWindowParam: marker.infoWindow.copyWith( + anchorParam: newAnchor, + ), + ); + }); + } + + Future _toggleDraggable(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith( + draggableParam: !marker.draggable, + ); + }); + } + + Future _toggleFlat(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith( + flatParam: !marker.flat, + ); + }); + } + + Future _changeInfo(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + final String newSnippet = '${marker.infoWindow.snippet!}*'; + setState(() { + markers[markerId] = marker.copyWith( + infoWindowParam: marker.infoWindow.copyWith( + snippetParam: newSnippet, + ), + ); + }); + } + + Future _changeAlpha(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + final double current = marker.alpha; + setState(() { + markers[markerId] = marker.copyWith( + alphaParam: current < 0.1 ? 1.0 : current * 0.75, + ); + }); + } + + Future _changeRotation(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + final double current = marker.rotation; + setState(() { + markers[markerId] = marker.copyWith( + rotationParam: current == 330.0 ? 0.0 : current + 30.0, + ); + }); + } + + Future _toggleVisible(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith( + visibleParam: !marker.visible, + ); + }); + } + + Future _changeZIndex(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + final double current = marker.zIndex; + setState(() { + markers[markerId] = marker.copyWith( + zIndexParam: current == 12.0 ? 0.0 : current + 1.0, + ); + }); + } + + void _setMarkerIcon(MarkerId markerId, BitmapDescriptor assetIcon) { + final Marker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith( + iconParam: assetIcon, + ); + }); + } + + Future _getAssetIcon(BuildContext context) async { + final Completer bitmapIcon = + Completer(); + final ImageConfiguration config = createLocalImageConfiguration(context); + + const AssetImage('assets/red_square.png') + .resolve(config) + .addListener(ImageStreamListener((ImageInfo image, bool sync) async { + final ByteData? bytes = + await image.image.toByteData(format: ImageByteFormat.png); + if (bytes == null) { + bitmapIcon.completeError(Exception('Unable to encode icon')); + return; + } + final BitmapDescriptor bitmap = + BitmapDescriptor.fromBytes(bytes.buffer.asUint8List()); + bitmapIcon.complete(bitmap); + })); + + return await bitmapIcon.future; + } + + @override + Widget build(BuildContext context) { + final MarkerId? selectedId = selectedMarker; + return Stack(children: [ + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ExampleGoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition( + target: LatLng(-33.852, 151.211), + zoom: 11.0, + ), + markers: Set.of(markers.values), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: _add, + child: const Text('Add'), + ), + TextButton( + onPressed: + selectedId == null ? null : () => _remove(selectedId), + child: const Text('Remove'), + ), + ], + ), + Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: + selectedId == null ? null : () => _changeInfo(selectedId), + child: const Text('change info'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _changeInfoAnchor(selectedId), + child: const Text('change info anchor'), + ), + TextButton( + onPressed: + selectedId == null ? null : () => _changeAlpha(selectedId), + child: const Text('change alpha'), + ), + TextButton( + onPressed: + selectedId == null ? null : () => _changeAnchor(selectedId), + child: const Text('change anchor'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _toggleDraggable(selectedId), + child: const Text('toggle draggable'), + ), + TextButton( + onPressed: + selectedId == null ? null : () => _toggleFlat(selectedId), + child: const Text('toggle flat'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _changePosition(selectedId), + child: const Text('change position'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _changeRotation(selectedId), + child: const Text('change rotation'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _toggleVisible(selectedId), + child: const Text('toggle visible'), + ), + TextButton( + onPressed: + selectedId == null ? null : () => _changeZIndex(selectedId), + child: const Text('change zIndex'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () { + _getAssetIcon(context).then( + (BitmapDescriptor icon) { + _setMarkerIcon(selectedId, icon); + }, + ); + }, + child: const Text('set marker icon'), + ), + ], + ), + ], + ), + Visibility( + visible: markerPosition != null, + child: Container( + color: Colors.white70, + height: 30, + padding: const EdgeInsets.only(left: 12, right: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + if (markerPosition == null) + Container() + else + Expanded(child: Text('lat: ${markerPosition!.latitude}')), + if (markerPosition == null) + Container() + else + Expanded(child: Text('lng: ${markerPosition!.longitude}')), + ], + ), + ), + ), + ]); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_polygon.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_polygon.dart new file mode 100644 index 000000000000..b41cb5d3ccb1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_polygon.dart @@ -0,0 +1,306 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class PlacePolygonPage extends GoogleMapExampleAppPage { + const PlacePolygonPage({Key? key}) + : super(const Icon(Icons.linear_scale), 'Place polygon', key: key); + + @override + Widget build(BuildContext context) { + return const PlacePolygonBody(); + } +} + +class PlacePolygonBody extends StatefulWidget { + const PlacePolygonBody({Key? key}) : super(key: key); + + @override + State createState() => PlacePolygonBodyState(); +} + +class PlacePolygonBodyState extends State { + PlacePolygonBodyState(); + + ExampleGoogleMapController? controller; + Map polygons = {}; + Map polygonOffsets = {}; + int _polygonIdCounter = 0; + PolygonId? selectedPolygon; + + // Values when toggling polygon color + int strokeColorsIndex = 0; + int fillColorsIndex = 0; + List colors = [ + Colors.purple, + Colors.red, + Colors.green, + Colors.pink, + ]; + + // Values when toggling polygon width + int widthsIndex = 0; + List widths = [10, 20, 5]; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _onPolygonTapped(PolygonId polygonId) { + setState(() { + selectedPolygon = polygonId; + }); + } + + void _remove(PolygonId polygonId) { + setState(() { + if (polygons.containsKey(polygonId)) { + polygons.remove(polygonId); + } + selectedPolygon = null; + }); + } + + void _add() { + final int polygonCount = polygons.length; + + if (polygonCount == 12) { + return; + } + + final String polygonIdVal = 'polygon_id_$_polygonIdCounter'; + final PolygonId polygonId = PolygonId(polygonIdVal); + + final Polygon polygon = Polygon( + polygonId: polygonId, + consumeTapEvents: true, + strokeColor: Colors.orange, + strokeWidth: 5, + fillColor: Colors.green, + points: _createPoints(), + onTap: () { + _onPolygonTapped(polygonId); + }, + ); + + setState(() { + polygons[polygonId] = polygon; + polygonOffsets[polygonId] = _polygonIdCounter.ceilToDouble(); + // increment _polygonIdCounter to have unique polygon id each time + _polygonIdCounter++; + }); + } + + void _toggleGeodesic(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + geodesicParam: !polygon.geodesic, + ); + }); + } + + void _toggleVisible(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + visibleParam: !polygon.visible, + ); + }); + } + + void _changeStrokeColor(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + strokeColorParam: colors[++strokeColorsIndex % colors.length], + ); + }); + } + + void _changeFillColor(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + fillColorParam: colors[++fillColorsIndex % colors.length], + ); + }); + } + + void _changeWidth(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + strokeWidthParam: widths[++widthsIndex % widths.length], + ); + }); + } + + void _addHoles(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = + polygon.copyWith(holesParam: _createHoles(polygonId)); + }); + } + + void _removeHoles(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + holesParam: >[], + ); + }); + } + + @override + Widget build(BuildContext context) { + final PolygonId? selectedId = selectedPolygon; + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: LatLng(52.4478, -3.5402), + zoom: 7.0, + ), + polygons: Set.of(polygons.values), + onMapCreated: _onMapCreated, + ), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Column( + children: [ + TextButton( + onPressed: _add, + child: const Text('add'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _remove(selectedId), + child: const Text('remove'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _toggleVisible(selectedId), + child: const Text('toggle visible'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _toggleGeodesic(selectedId), + child: const Text('toggle geodesic'), + ), + ], + ), + Column( + children: [ + TextButton( + onPressed: (selectedId == null) + ? null + : ((polygons[selectedId]!.holes.isNotEmpty) + ? null + : () => _addHoles(selectedId)), + child: const Text('add holes'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : ((polygons[selectedId]!.holes.isEmpty) + ? null + : () => _removeHoles(selectedId)), + child: const Text('remove holes'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeWidth(selectedId), + child: const Text('change stroke width'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeStrokeColor(selectedId), + child: const Text('change stroke color'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeFillColor(selectedId), + child: const Text('change fill color'), + ), + ], + ) + ], + ) + ], + ), + ), + ), + ], + ); + } + + List _createPoints() { + final List points = []; + final double offset = _polygonIdCounter.ceilToDouble(); + points.add(_createLatLng(51.2395 + offset, -3.4314)); + points.add(_createLatLng(53.5234 + offset, -3.5314)); + points.add(_createLatLng(52.4351 + offset, -4.5235)); + points.add(_createLatLng(52.1231 + offset, -5.0829)); + return points; + } + + List> _createHoles(PolygonId polygonId) { + final List> holes = >[]; + final double offset = polygonOffsets[polygonId]!; + + final List hole1 = []; + hole1.add(_createLatLng(51.8395 + offset, -3.8814)); + hole1.add(_createLatLng(52.0234 + offset, -3.9914)); + hole1.add(_createLatLng(52.1351 + offset, -4.4435)); + hole1.add(_createLatLng(52.0231 + offset, -4.5829)); + holes.add(hole1); + + final List hole2 = []; + hole2.add(_createLatLng(52.2395 + offset, -3.6814)); + hole2.add(_createLatLng(52.4234 + offset, -3.7914)); + hole2.add(_createLatLng(52.5351 + offset, -4.2435)); + hole2.add(_createLatLng(52.4231 + offset, -4.3829)); + holes.add(hole2); + + return holes; + } + + LatLng _createLatLng(double lat, double lng) { + return LatLng(lat, lng); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_polyline.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_polyline.dart new file mode 100644 index 000000000000..004206b9f6cc --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_polyline.dart @@ -0,0 +1,325 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class PlacePolylinePage extends GoogleMapExampleAppPage { + const PlacePolylinePage({Key? key}) + : super(const Icon(Icons.linear_scale), 'Place polyline', key: key); + + @override + Widget build(BuildContext context) { + return const PlacePolylineBody(); + } +} + +class PlacePolylineBody extends StatefulWidget { + const PlacePolylineBody({Key? key}) : super(key: key); + + @override + State createState() => PlacePolylineBodyState(); +} + +class PlacePolylineBodyState extends State { + PlacePolylineBodyState(); + + ExampleGoogleMapController? controller; + Map polylines = {}; + int _polylineIdCounter = 0; + PolylineId? selectedPolyline; + + // Values when toggling polyline color + int colorsIndex = 0; + List colors = [ + Colors.purple, + Colors.red, + Colors.green, + Colors.pink, + ]; + + // Values when toggling polyline width + int widthsIndex = 0; + List widths = [10, 20, 5]; + + int jointTypesIndex = 0; + List jointTypes = [ + JointType.mitered, + JointType.bevel, + JointType.round + ]; + + // Values when toggling polyline end cap type + int endCapsIndex = 0; + List endCaps = [Cap.buttCap, Cap.squareCap, Cap.roundCap]; + + // Values when toggling polyline start cap type + int startCapsIndex = 0; + List startCaps = [Cap.buttCap, Cap.squareCap, Cap.roundCap]; + + // Values when toggling polyline pattern + int patternsIndex = 0; + List> patterns = >[ + [], + [ + PatternItem.dash(30.0), + PatternItem.gap(20.0), + PatternItem.dot, + PatternItem.gap(20.0) + ], + [PatternItem.dash(30.0), PatternItem.gap(20.0)], + [PatternItem.dot, PatternItem.gap(10.0)], + ]; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _onPolylineTapped(PolylineId polylineId) { + setState(() { + selectedPolyline = polylineId; + }); + } + + void _remove(PolylineId polylineId) { + setState(() { + if (polylines.containsKey(polylineId)) { + polylines.remove(polylineId); + } + selectedPolyline = null; + }); + } + + void _add() { + final int polylineCount = polylines.length; + + if (polylineCount == 12) { + return; + } + + final String polylineIdVal = 'polyline_id_$_polylineIdCounter'; + _polylineIdCounter++; + final PolylineId polylineId = PolylineId(polylineIdVal); + + final Polyline polyline = Polyline( + polylineId: polylineId, + consumeTapEvents: true, + color: Colors.orange, + width: 5, + points: _createPoints(), + onTap: () { + _onPolylineTapped(polylineId); + }, + ); + + setState(() { + polylines[polylineId] = polyline; + }); + } + + void _toggleGeodesic(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + geodesicParam: !polyline.geodesic, + ); + }); + } + + void _toggleVisible(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + visibleParam: !polyline.visible, + ); + }); + } + + void _changeColor(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + colorParam: colors[++colorsIndex % colors.length], + ); + }); + } + + void _changeWidth(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + widthParam: widths[++widthsIndex % widths.length], + ); + }); + } + + void _changeJointType(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + jointTypeParam: jointTypes[++jointTypesIndex % jointTypes.length], + ); + }); + } + + void _changeEndCap(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + endCapParam: endCaps[++endCapsIndex % endCaps.length], + ); + }); + } + + void _changeStartCap(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + startCapParam: startCaps[++startCapsIndex % startCaps.length], + ); + }); + } + + void _changePattern(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; + setState(() { + polylines[polylineId] = polyline.copyWith( + patternsParam: patterns[++patternsIndex % patterns.length], + ); + }); + } + + @override + Widget build(BuildContext context) { + final bool isIOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; + + final PolylineId? selectedId = selectedPolyline; + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: LatLng(53.1721, -3.5402), + zoom: 7.0, + ), + polylines: Set.of(polylines.values), + onMapCreated: _onMapCreated, + ), + ), + ), + Expanded( + child: SingleChildScrollView( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Column( + children: [ + TextButton( + onPressed: _add, + child: const Text('add'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _remove(selectedId), + child: const Text('remove'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _toggleVisible(selectedId), + child: const Text('toggle visible'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _toggleGeodesic(selectedId), + child: const Text('toggle geodesic'), + ), + ], + ), + Column( + children: [ + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeWidth(selectedId), + child: const Text('change width'), + ), + TextButton( + onPressed: (selectedId == null) + ? null + : () => _changeColor(selectedId), + child: const Text('change color'), + ), + TextButton( + onPressed: isIOS || (selectedId == null) + ? null + : () => _changeStartCap(selectedId), + child: const Text('change start cap [Android only]'), + ), + TextButton( + onPressed: isIOS || (selectedId == null) + ? null + : () => _changeEndCap(selectedId), + child: const Text('change end cap [Android only]'), + ), + TextButton( + onPressed: isIOS || (selectedId == null) + ? null + : () => _changeJointType(selectedId), + child: const Text('change joint type [Android only]'), + ), + TextButton( + onPressed: isIOS || (selectedId == null) + ? null + : () => _changePattern(selectedId), + child: const Text('change pattern [Android only]'), + ), + ], + ) + ], + ) + ], + ), + ), + ), + ], + ); + } + + List _createPoints() { + final List points = []; + final double offset = _polylineIdCounter.ceilToDouble(); + points.add(_createLatLng(51.4816 + offset, -3.1791)); + points.add(_createLatLng(53.0430 + offset, -2.9925)); + points.add(_createLatLng(53.1396 + offset, -4.2739)); + points.add(_createLatLng(52.4153 + offset, -4.0829)); + return points; + } + + LatLng _createLatLng(double lat, double lng) { + return LatLng(lat, lng); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/scrolling_map.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/scrolling_map.dart new file mode 100644 index 000000000000..7a9b75cd1224 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/scrolling_map.dart @@ -0,0 +1,114 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +const LatLng _center = LatLng(32.080664, 34.9563837); + +class ScrollingMapPage extends GoogleMapExampleAppPage { + const ScrollingMapPage({Key? key}) + : super(const Icon(Icons.map), 'Scrolling map', key: key); + + @override + Widget build(BuildContext context) { + return const ScrollingMapBody(); + } +} + +class ScrollingMapBody extends StatelessWidget { + const ScrollingMapBody({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListView( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 30.0), + child: Column( + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 12.0), + child: Text('This map consumes all touch events.'), + ), + Center( + child: SizedBox( + width: 300.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: _center, + zoom: 11.0, + ), + gestureRecognizers: // + >{ + Factory( + () => EagerGestureRecognizer(), + ), + }, + ), + ), + ), + ], + ), + ), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 30.0), + child: Column( + children: [ + const Text("This map doesn't consume the vertical drags."), + const Padding( + padding: EdgeInsets.only(bottom: 12.0), + child: + Text('It still gets other gestures (e.g scale or tap).'), + ), + Center( + child: SizedBox( + width: 300.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: _center, + zoom: 11.0, + ), + markers: { + Marker( + markerId: const MarkerId('test_marker_id'), + position: LatLng( + _center.latitude, + _center.longitude, + ), + infoWindow: const InfoWindow( + title: 'An interesting location', + snippet: '*', + ), + ), + }, + gestureRecognizers: < + Factory>{ + Factory( + () => ScaleGestureRecognizer(), + ), + }, + ), + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/snapshot.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/snapshot.dart new file mode 100644 index 000000000000..56a90a8e49f6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/snapshot.dart @@ -0,0 +1,76 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +const CameraPosition _kInitialPosition = + CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); + +class SnapshotPage extends GoogleMapExampleAppPage { + const SnapshotPage({Key? key}) + : super(const Icon(Icons.camera_alt), 'Take a snapshot of the map', + key: key); + + @override + Widget build(BuildContext context) { + return _SnapshotBody(); + } +} + +class _SnapshotBody extends StatefulWidget { + @override + _SnapshotBodyState createState() => _SnapshotBodyState(); +} + +class _SnapshotBodyState extends State<_SnapshotBody> { + ExampleGoogleMapController? _mapController; + Uint8List? _imageBytes; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + height: 180, + child: ExampleGoogleMap( + onMapCreated: onMapCreated, + initialCameraPosition: _kInitialPosition, + ), + ), + TextButton( + child: const Text('Take a snapshot'), + onPressed: () async { + final Uint8List? imageBytes = + await _mapController?.takeSnapshot(); + setState(() { + _imageBytes = imageBytes; + }); + }, + ), + Container( + decoration: BoxDecoration(color: Colors.blueGrey[50]), + height: 180, + child: _imageBytes != null ? Image.memory(_imageBytes!) : null, + ), + ], + ), + ); + } + + // ignore: use_setters_to_change_properties + void onMapCreated(ExampleGoogleMapController controller) { + _mapController = controller; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/tile_overlay.dart new file mode 100644 index 000000000000..e25ab916d8de --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/tile_overlay.dart @@ -0,0 +1,154 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class TileOverlayPage extends GoogleMapExampleAppPage { + const TileOverlayPage({Key? key}) + : super(const Icon(Icons.map), 'Tile overlay', key: key); + + @override + Widget build(BuildContext context) { + return const TileOverlayBody(); + } +} + +class TileOverlayBody extends StatefulWidget { + const TileOverlayBody({Key? key}) : super(key: key); + + @override + State createState() => TileOverlayBodyState(); +} + +class TileOverlayBodyState extends State { + TileOverlayBodyState(); + + ExampleGoogleMapController? controller; + TileOverlay? _tileOverlay; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _removeTileOverlay() { + setState(() { + _tileOverlay = null; + }); + } + + void _addTileOverlay() { + final TileOverlay tileOverlay = TileOverlay( + tileOverlayId: const TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + ); + setState(() { + _tileOverlay = tileOverlay; + }); + } + + void _clearTileCache() { + if (_tileOverlay != null && controller != null) { + controller!.clearTileCache(_tileOverlay!.tileOverlayId); + } + } + + @override + Widget build(BuildContext context) { + final Set overlays = { + if (_tileOverlay != null) _tileOverlay!, + }; + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: LatLng(59.935460, 30.325177), + zoom: 7.0, + ), + tileOverlays: overlays, + onMapCreated: _onMapCreated, + ), + ), + ), + TextButton( + onPressed: _addTileOverlay, + child: const Text('Add tile overlay'), + ), + TextButton( + onPressed: _removeTileOverlay, + child: const Text('Remove tile overlay'), + ), + TextButton( + onPressed: _clearTileCache, + child: const Text('Clear tile cache'), + ), + ], + ); + } +} + +class _DebugTileProvider implements TileProvider { + _DebugTileProvider() { + boxPaint.isAntiAlias = true; + boxPaint.color = Colors.blue; + boxPaint.strokeWidth = 2.0; + boxPaint.style = PaintingStyle.stroke; + } + + static const int width = 100; + static const int height = 100; + static final Paint boxPaint = Paint(); + static const TextStyle textStyle = TextStyle( + color: Colors.red, + fontSize: 20, + ); + + @override + Future getTile(int x, int y, int? zoom) async { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final TextSpan textSpan = TextSpan( + text: '$x,$y', + style: textStyle, + ); + final TextPainter textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + ); + textPainter.layout( + maxWidth: width.toDouble(), + ); + textPainter.paint(canvas, Offset.zero); + canvas.drawRect( + Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint); + final ui.Picture picture = recorder.endRecording(); + final Uint8List byteData = await picture + .toImage(width, height) + .then((ui.Image image) => + image.toByteData(format: ui.ImageByteFormat.png)) + .then((ByteData? byteData) => byteData!.buffer.asUint8List()); + return Tile(width, height, byteData); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml new file mode 100644 index 000000000000..ae61f5d92f3c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml @@ -0,0 +1,34 @@ +name: google_maps_flutter_example +description: Demonstrates how to use the google_maps_flutter plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + cupertino_icons: ^0.1.0 + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.1 + google_maps_flutter_ios: + # When depending on this package from a real application you should use: + # google_maps_flutter_ios: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + google_maps_flutter_platform_interface: ^2.2.1 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true + assets: + - assets/ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/test_driver/integration_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/test_driver/integration_test.dart @@ -0,0 +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. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/google_sign_in/google_sign_in/ios/Assets/.gitkeep b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Assets/.gitkeep similarity index 100% rename from packages/google_sign_in/google_sign_in/ios/Assets/.gitkeep rename to packages/google_maps_flutter/google_maps_flutter_ios/ios/Assets/.gitkeep diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h new file mode 100644 index 000000000000..cfccb7b0b5f9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h @@ -0,0 +1,30 @@ +// 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTGoogleMapJSONConversions : NSObject + ++ (CLLocationCoordinate2D)locationFromLatLong:(NSArray *)latlong; ++ (CGPoint)pointFromArray:(NSArray *)array; ++ (NSArray *)arrayFromLocation:(CLLocationCoordinate2D)location; ++ (UIColor *)colorFromRGBA:(NSNumber *)data; ++ (NSArray *)pointsFromLatLongs:(NSArray *)data; ++ (NSArray *> *)holesFromPointsArray:(NSArray *)data; ++ (nullable NSDictionary *)dictionaryFromPosition: + (nullable GMSCameraPosition *)position; ++ (NSDictionary *)dictionaryFromPoint:(CGPoint)point; ++ (nullable NSDictionary *)dictionaryFromCoordinateBounds:(nullable GMSCoordinateBounds *)bounds; ++ (nullable GMSCameraPosition *)cameraPostionFromDictionary:(nullable NSDictionary *)channelValue; ++ (CGPoint)pointFromDictionary:(NSDictionary *)dictionary; ++ (GMSCoordinateBounds *)coordinateBoundsFromLatLongs:(NSArray *)latlongs; ++ (GMSMapViewType)mapViewTypeFromTypeValue:(NSNumber *)value; ++ (nullable GMSCameraUpdate *)cameraUpdateFromChannelValue:(NSArray *)channelValue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m new file mode 100644 index 000000000000..d554c501b1e2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m @@ -0,0 +1,144 @@ +// 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 "FLTGoogleMapJSONConversions.h" + +@implementation FLTGoogleMapJSONConversions + ++ (CLLocationCoordinate2D)locationFromLatLong:(NSArray *)latlong { + return CLLocationCoordinate2DMake([latlong[0] doubleValue], [latlong[1] doubleValue]); +} + ++ (CGPoint)pointFromArray:(NSArray *)array { + return CGPointMake([array[0] doubleValue], [array[1] doubleValue]); +} + ++ (NSArray *)arrayFromLocation:(CLLocationCoordinate2D)location { + return @[ @(location.latitude), @(location.longitude) ]; +} + ++ (UIColor *)colorFromRGBA:(NSNumber *)numberColor { + unsigned long value = [numberColor unsignedLongValue]; + return [UIColor colorWithRed:((float)((value & 0xFF0000) >> 16)) / 255.0 + green:((float)((value & 0xFF00) >> 8)) / 255.0 + blue:((float)(value & 0xFF)) / 255.0 + alpha:((float)((value & 0xFF000000) >> 24)) / 255.0]; +} + ++ (NSArray *)pointsFromLatLongs:(NSArray *)data { + NSMutableArray *points = [[NSMutableArray alloc] init]; + for (unsigned i = 0; i < [data count]; i++) { + NSNumber *latitude = data[i][0]; + NSNumber *longitude = data[i][1]; + CLLocation *point = [[CLLocation alloc] initWithLatitude:[latitude doubleValue] + longitude:[longitude doubleValue]]; + [points addObject:point]; + } + + return points; +} + ++ (NSArray *> *)holesFromPointsArray:(NSArray *)data { + NSMutableArray *> *holes = [[[NSMutableArray alloc] init] init]; + for (unsigned i = 0; i < [data count]; i++) { + NSArray *points = [FLTGoogleMapJSONConversions pointsFromLatLongs:data[i]]; + [holes addObject:points]; + } + + return holes; +} + ++ (nullable NSDictionary *)dictionaryFromPosition:(GMSCameraPosition *)position { + if (!position) { + return nil; + } + return @{ + @"target" : [FLTGoogleMapJSONConversions arrayFromLocation:[position target]], + @"zoom" : @([position zoom]), + @"bearing" : @([position bearing]), + @"tilt" : @([position viewingAngle]), + }; +} + ++ (NSDictionary *)dictionaryFromPoint:(CGPoint)point { + return @{ + @"x" : @(lroundf(point.x)), + @"y" : @(lroundf(point.y)), + }; +} + ++ (nullable NSDictionary *)dictionaryFromCoordinateBounds:(GMSCoordinateBounds *)bounds { + if (!bounds) { + return nil; + } + return @{ + @"southwest" : [FLTGoogleMapJSONConversions arrayFromLocation:[bounds southWest]], + @"northeast" : [FLTGoogleMapJSONConversions arrayFromLocation:[bounds northEast]], + }; +} + ++ (nullable GMSCameraPosition *)cameraPostionFromDictionary:(nullable NSDictionary *)data { + if (!data) { + return nil; + } + return [GMSCameraPosition + cameraWithTarget:[FLTGoogleMapJSONConversions locationFromLatLong:data[@"target"]] + zoom:[data[@"zoom"] floatValue] + bearing:[data[@"bearing"] doubleValue] + viewingAngle:[data[@"tilt"] doubleValue]]; +} + ++ (CGPoint)pointFromDictionary:(NSDictionary *)dictionary { + double x = [dictionary[@"x"] doubleValue]; + double y = [dictionary[@"y"] doubleValue]; + return CGPointMake(x, y); +} + ++ (GMSCoordinateBounds *)coordinateBoundsFromLatLongs:(NSArray *)latlongs { + return [[GMSCoordinateBounds alloc] + initWithCoordinate:[FLTGoogleMapJSONConversions locationFromLatLong:latlongs[0]] + coordinate:[FLTGoogleMapJSONConversions locationFromLatLong:latlongs[1]]]; +} + ++ (GMSMapViewType)mapViewTypeFromTypeValue:(NSNumber *)typeValue { + int value = [typeValue intValue]; + return (GMSMapViewType)(value == 0 ? 5 : value); +} + ++ (nullable GMSCameraUpdate *)cameraUpdateFromChannelValue:(NSArray *)channelValue { + NSString *update = channelValue[0]; + if ([update isEqualToString:@"newCameraPosition"]) { + return [GMSCameraUpdate + setCamera:[FLTGoogleMapJSONConversions cameraPostionFromDictionary:channelValue[1]]]; + } else if ([update isEqualToString:@"newLatLng"]) { + return [GMSCameraUpdate + setTarget:[FLTGoogleMapJSONConversions locationFromLatLong:channelValue[1]]]; + } else if ([update isEqualToString:@"newLatLngBounds"]) { + return [GMSCameraUpdate + fitBounds:[FLTGoogleMapJSONConversions coordinateBoundsFromLatLongs:channelValue[1]] + withPadding:[channelValue[2] doubleValue]]; + } else if ([update isEqualToString:@"newLatLngZoom"]) { + return + [GMSCameraUpdate setTarget:[FLTGoogleMapJSONConversions locationFromLatLong:channelValue[1]] + zoom:[channelValue[2] floatValue]]; + } else if ([update isEqualToString:@"scrollBy"]) { + return [GMSCameraUpdate scrollByX:[channelValue[1] doubleValue] + Y:[channelValue[2] doubleValue]]; + } else if ([update isEqualToString:@"zoomBy"]) { + if (channelValue.count == 2) { + return [GMSCameraUpdate zoomBy:[channelValue[1] floatValue]]; + } else { + return [GMSCameraUpdate zoomBy:[channelValue[1] floatValue] + atPoint:[FLTGoogleMapJSONConversions pointFromArray:channelValue[2]]]; + } + } else if ([update isEqualToString:@"zoomIn"]) { + return [GMSCameraUpdate zoomIn]; + } else if ([update isEqualToString:@"zoomOut"]) { + return [GMSCameraUpdate zoomOut]; + } else if ([update isEqualToString:@"zoomTo"]) { + return [GMSCameraUpdate zoomTo:[channelValue[1] floatValue]]; + } + return nil; +} +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapTileOverlayController.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapTileOverlayController.h new file mode 100644 index 000000000000..5dcc66594f18 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapTileOverlayController.h @@ -0,0 +1,36 @@ +// 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTGoogleMapTileOverlayController : NSObject +- (instancetype)initWithTileLayer:(GMSTileLayer *)tileLayer + mapView:(GMSMapView *)mapView + options:(NSDictionary *)optionsData; +- (void)removeTileOverlay; +- (void)clearTileCache; +- (NSDictionary *)getTileOverlayInfo; +@end + +@interface FLTTileProviderController : GMSTileLayer +@property(copy, nonatomic, readonly) NSString *tileOverlayIdentifier; +- (instancetype)init:(FlutterMethodChannel *)methodChannel + withTileOverlayIdentifier:(NSString *)identifier; +@end + +@interface FLTTileOverlaysController : NSObject +- (instancetype)init:(FlutterMethodChannel *)methodChannel + mapView:(GMSMapView *)mapView + registrar:(NSObject *)registrar; +- (void)addTileOverlays:(NSArray *)tileOverlaysToAdd; +- (void)changeTileOverlays:(NSArray *)tileOverlaysToChange; +- (void)removeTileOverlayWithIdentifiers:(NSArray *)identifiers; +- (void)clearTileCacheWithIdentifier:(NSString *)identifier; +- (nullable NSDictionary *)tileOverlayInfoWithIdentifier:(NSString *)identifier; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapTileOverlayController.m similarity index 60% rename from packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m rename to packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapTileOverlayController.m index 6baa753ef999..5863697d7b9b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapTileOverlayController.m @@ -3,36 +3,7 @@ // found in the LICENSE file. #import "FLTGoogleMapTileOverlayController.h" -#import "JsonConversions.h" - -static void InterpretTileOverlayOptions(NSDictionary *data, - id sink, - NSObject *registrar) { - NSNumber *visible = data[@"visible"]; - if (visible != nil) { - [sink setVisible:visible.boolValue]; - } - - NSNumber *transparency = data[@"transparency"]; - if (transparency != nil) { - [sink setTransparency:transparency.floatValue]; - } - - NSNumber *zIndex = data[@"zIndex"]; - if (zIndex != nil) { - [sink setZIndex:zIndex.intValue]; - } - - NSNumber *fadeIn = data[@"fadeIn"]; - if (fadeIn != nil) { - [sink setFadeIn:fadeIn.boolValue]; - } - - NSNumber *tileSize = data[@"tileSize"]; - if (tileSize != nil) { - [sink setTileSize:tileSize.integerValue]; - } -} +#import "FLTGoogleMapJSONConversions.h" @interface FLTGoogleMapTileOverlayController () @@ -43,11 +14,14 @@ @interface FLTGoogleMapTileOverlayController () @implementation FLTGoogleMapTileOverlayController -- (instancetype)initWithTileLayer:(GMSTileLayer *)tileLayer mapView:(GMSMapView *)mapView { +- (instancetype)initWithTileLayer:(GMSTileLayer *)tileLayer + mapView:(GMSMapView *)mapView + options:(NSDictionary *)optionsData { self = [super init]; if (self) { - self.layer = tileLayer; - self.mapView = mapView; + _layer = tileLayer; + _mapView = mapView; + [self interpretTileOverlayOptions:optionsData]; } return self; } @@ -71,8 +45,6 @@ - (NSDictionary *)getTileOverlayInfo { return info; } -#pragma mark - FLTGoogleMapTileOverlayOptionsSink methods - - (void)setFadeIn:(BOOL)fadeIn { self.layer.fadeIn = fadeIn; } @@ -93,22 +65,53 @@ - (void)setZIndex:(int)zIndex { - (void)setTileSize:(NSInteger)tileSize { self.layer.tileSize = tileSize; } + +- (void)interpretTileOverlayOptions:(NSDictionary *)data { + if (!data) { + return; + } + NSNumber *visible = data[@"visible"]; + if (visible != nil && visible != (id)[NSNull null]) { + [self setVisible:visible.boolValue]; + } + + NSNumber *transparency = data[@"transparency"]; + if (transparency != nil && transparency != (id)[NSNull null]) { + [self setTransparency:transparency.floatValue]; + } + + NSNumber *zIndex = data[@"zIndex"]; + if (zIndex != nil && zIndex != (id)[NSNull null]) { + [self setZIndex:zIndex.intValue]; + } + + NSNumber *fadeIn = data[@"fadeIn"]; + if (fadeIn != nil && fadeIn != (id)[NSNull null]) { + [self setFadeIn:fadeIn.boolValue]; + } + + NSNumber *tileSize = data[@"tileSize"]; + if (tileSize != nil && tileSize != (id)[NSNull null]) { + [self setTileSize:tileSize.integerValue]; + } +} + @end @interface FLTTileProviderController () -@property(weak, nonatomic) FlutterMethodChannel *methodChannel; -@property(copy, nonatomic, readwrite) NSString *tileOverlayId; +@property(strong, nonatomic) FlutterMethodChannel *methodChannel; @end @implementation FLTTileProviderController -- (instancetype)init:(FlutterMethodChannel *)methodChannel tileOverlayId:(NSString *)tileOverlayId { +- (instancetype)init:(FlutterMethodChannel *)methodChannel + withTileOverlayIdentifier:(NSString *)identifier { self = [super init]; if (self) { - self.methodChannel = methodChannel; - self.tileOverlayId = tileOverlayId; + _methodChannel = methodChannel; + _tileOverlayIdentifier = identifier; } return self; } @@ -122,7 +125,7 @@ - (void)requestTileForX:(NSUInteger)x [self.methodChannel invokeMethod:@"tileOverlay#getTile" arguments:@{ - @"tileOverlayId" : self.tileOverlayId, + @"tileOverlayId" : self.tileOverlayIdentifier, @"x" : @(x), @"y" : @(y), @"zoom" : @(zoom) @@ -156,9 +159,8 @@ - (void)requestTileForX:(NSUInteger)x @interface FLTTileOverlaysController () -@property(strong, nonatomic) NSMutableDictionary *tileOverlayIdToController; -@property(weak, nonatomic) FlutterMethodChannel *methodChannel; -@property(weak, nonatomic) NSObject *registrar; +@property(strong, nonatomic) NSMutableDictionary *tileOverlayIdentifierToController; +@property(strong, nonatomic) FlutterMethodChannel *methodChannel; @property(weak, nonatomic) GMSMapView *mapView; @end @@ -170,64 +172,67 @@ - (instancetype)init:(FlutterMethodChannel *)methodChannel registrar:(NSObject *)registrar { self = [super init]; if (self) { - self.methodChannel = methodChannel; - self.mapView = mapView; - self.tileOverlayIdToController = [[NSMutableDictionary alloc] init]; - self.registrar = registrar; + _methodChannel = methodChannel; + _mapView = mapView; + _tileOverlayIdentifierToController = [[NSMutableDictionary alloc] init]; } return self; } - (void)addTileOverlays:(NSArray *)tileOverlaysToAdd { for (NSDictionary *tileOverlay in tileOverlaysToAdd) { - NSString *tileOverlayId = [FLTTileOverlaysController getTileOverlayId:tileOverlay]; + NSString *identifier = [FLTTileOverlaysController identifierForTileOverlay:tileOverlay]; FLTTileProviderController *tileProvider = - [[FLTTileProviderController alloc] init:self.methodChannel tileOverlayId:tileOverlayId]; + [[FLTTileProviderController alloc] init:self.methodChannel + withTileOverlayIdentifier:identifier]; FLTGoogleMapTileOverlayController *controller = [[FLTGoogleMapTileOverlayController alloc] initWithTileLayer:tileProvider - mapView:self.mapView]; - InterpretTileOverlayOptions(tileOverlay, controller, self.registrar); - self.tileOverlayIdToController[tileOverlayId] = controller; + mapView:self.mapView + options:tileOverlay]; + self.tileOverlayIdentifierToController[identifier] = controller; } } - (void)changeTileOverlays:(NSArray *)tileOverlaysToChange { for (NSDictionary *tileOverlay in tileOverlaysToChange) { - NSString *tileOverlayId = [FLTTileOverlaysController getTileOverlayId:tileOverlay]; - FLTGoogleMapTileOverlayController *controller = self.tileOverlayIdToController[tileOverlayId]; + NSString *identifier = [FLTTileOverlaysController identifierForTileOverlay:tileOverlay]; + FLTGoogleMapTileOverlayController *controller = + self.tileOverlayIdentifierToController[identifier]; if (!controller) { continue; } - InterpretTileOverlayOptions(tileOverlay, controller, self.registrar); + [controller interpretTileOverlayOptions:tileOverlay]; } } -- (void)removeTileOverlayIds:(NSArray *)tileOverlayIdsToRemove { - for (NSString *tileOverlayId in tileOverlayIdsToRemove) { - FLTGoogleMapTileOverlayController *controller = self.tileOverlayIdToController[tileOverlayId]; +- (void)removeTileOverlayWithIdentifiers:(NSArray *)identifiers { + for (NSString *identifier in identifiers) { + FLTGoogleMapTileOverlayController *controller = + self.tileOverlayIdentifierToController[identifier]; if (!controller) { continue; } [controller removeTileOverlay]; - [self.tileOverlayIdToController removeObjectForKey:tileOverlayId]; + [self.tileOverlayIdentifierToController removeObjectForKey:identifier]; } } -- (void)clearTileCache:(NSString *)tileOverlayId { - FLTGoogleMapTileOverlayController *controller = self.tileOverlayIdToController[tileOverlayId]; +- (void)clearTileCacheWithIdentifier:(NSString *)identifier { + FLTGoogleMapTileOverlayController *controller = + self.tileOverlayIdentifierToController[identifier]; if (!controller) { return; } [controller clearTileCache]; } -- (nullable NSDictionary *)getTileOverlayInfo:(NSString *)tileverlayId { - if (self.tileOverlayIdToController[tileverlayId] == nil) { +- (nullable NSDictionary *)tileOverlayInfoWithIdentifier:(NSString *)identifier { + if (self.tileOverlayIdentifierToController[identifier] == nil) { return nil; } - return [self.tileOverlayIdToController[tileverlayId] getTileOverlayInfo]; + return [self.tileOverlayIdentifierToController[identifier] getTileOverlayInfo]; } -+ (NSString *)getTileOverlayId:(NSDictionary *)tileOverlay { ++ (NSString *)identifierForTileOverlay:(NSDictionary *)tileOverlay { return tileOverlay[@"tileOverlayId"]; } diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.h similarity index 90% rename from packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.h rename to packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.h index 953c0557ff20..26f69eaf3882 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.h @@ -10,5 +10,9 @@ #import "GoogleMapPolygonController.h" #import "GoogleMapPolylineController.h" +NS_ASSUME_NONNULL_BEGIN + @interface FLTGoogleMapsPlugin : NSObject @end + +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.m similarity index 51% rename from packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.m rename to packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.m index e78a505ecfb0..70bde9022a0d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.m @@ -6,26 +6,14 @@ #pragma mark - GoogleMaps plugin implementation -@implementation FLTGoogleMapsPlugin { - NSObject *_registrar; - FlutterMethodChannel *_channel; - NSMutableDictionary *_mapControllers; -} +@implementation FLTGoogleMapsPlugin + (void)registerWithRegistrar:(NSObject *)registrar { FLTGoogleMapFactory *googleMapFactory = [[FLTGoogleMapFactory alloc] initWithRegistrar:registrar]; [registrar registerViewFactory:googleMapFactory - withId:@"plugins.flutter.io/google_maps" + withId:@"plugins.flutter.dev/google_maps_ios" gestureRecognizersBlockingPolicy: FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded]; } -- (FLTGoogleMapController *)mapFromCall:(FlutterMethodCall *)call error:(FlutterError **)error { - id mapId = call.arguments[@"map"]; - FLTGoogleMapController *controller = _mapControllers[mapId]; - if (!controller && error) { - *error = [FlutterError errorWithCode:@"unknown_map" message:nil details:mapId]; - } - return controller; -} @end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapCircleController.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapCircleController.h new file mode 100644 index 000000000000..6b67760fdaff --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapCircleController.h @@ -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 +#import + +// Defines circle controllable by Flutter. +@interface FLTGoogleMapCircleController : NSObject +- (instancetype)initCircleWithPosition:(CLLocationCoordinate2D)position + radius:(CLLocationDistance)radius + circleId:(NSString *)circleIdentifier + mapView:(GMSMapView *)mapView + options:(NSDictionary *)options; +- (void)removeCircle; +@end + +@interface FLTCirclesController : NSObject +- (instancetype)init:(FlutterMethodChannel *)methodChannel + mapView:(GMSMapView *)mapView + registrar:(NSObject *)registrar; +- (void)addCircles:(NSArray *)circlesToAdd; +- (void)changeCircles:(NSArray *)circlesToChange; +- (void)removeCircleWithIdentifiers:(NSArray *)identifiers; +- (void)didTapCircleWithIdentifier:(NSString *)identifier; +- (bool)hasCircleWithIdentifier:(NSString *)identifier; +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapCircleController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapCircleController.m new file mode 100644 index 000000000000..53bf69075c95 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapCircleController.m @@ -0,0 +1,197 @@ +// 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 "GoogleMapCircleController.h" +#import "FLTGoogleMapJSONConversions.h" + +@interface FLTGoogleMapCircleController () + +@property(nonatomic, strong) GMSCircle *circle; +@property(nonatomic, weak) GMSMapView *mapView; + +@end + +@implementation FLTGoogleMapCircleController + +- (instancetype)initCircleWithPosition:(CLLocationCoordinate2D)position + radius:(CLLocationDistance)radius + circleId:(NSString *)circleIdentifier + mapView:(GMSMapView *)mapView + options:(NSDictionary *)options { + self = [super init]; + if (self) { + _circle = [GMSCircle circleWithPosition:position radius:radius]; + _mapView = mapView; + _circle.userData = @[ circleIdentifier ]; + [self interpretCircleOptions:options]; + } + return self; +} + +- (void)removeCircle { + self.circle.map = nil; +} + +- (void)setConsumeTapEvents:(BOOL)consumes { + self.circle.tappable = consumes; +} +- (void)setVisible:(BOOL)visible { + self.circle.map = visible ? self.mapView : nil; +} +- (void)setZIndex:(int)zIndex { + self.circle.zIndex = zIndex; +} +- (void)setCenter:(CLLocationCoordinate2D)center { + self.circle.position = center; +} +- (void)setRadius:(CLLocationDistance)radius { + self.circle.radius = radius; +} + +- (void)setStrokeColor:(UIColor *)color { + self.circle.strokeColor = color; +} +- (void)setStrokeWidth:(CGFloat)width { + self.circle.strokeWidth = width; +} +- (void)setFillColor:(UIColor *)color { + self.circle.fillColor = color; +} + +- (void)interpretCircleOptions:(NSDictionary *)data { + NSNumber *consumeTapEvents = data[@"consumeTapEvents"]; + if (consumeTapEvents && consumeTapEvents != (id)[NSNull null]) { + [self setConsumeTapEvents:consumeTapEvents.boolValue]; + } + + NSNumber *visible = data[@"visible"]; + if (visible && visible != (id)[NSNull null]) { + [self setVisible:[visible boolValue]]; + } + + NSNumber *zIndex = data[@"zIndex"]; + if (zIndex && zIndex != (id)[NSNull null]) { + [self setZIndex:[zIndex intValue]]; + } + + NSArray *center = data[@"center"]; + if (center && center != (id)[NSNull null]) { + [self setCenter:[FLTGoogleMapJSONConversions locationFromLatLong:center]]; + } + + NSNumber *radius = data[@"radius"]; + if (radius && radius != (id)[NSNull null]) { + [self setRadius:[radius floatValue]]; + } + + NSNumber *strokeColor = data[@"strokeColor"]; + if (strokeColor && strokeColor != (id)[NSNull null]) { + [self setStrokeColor:[FLTGoogleMapJSONConversions colorFromRGBA:strokeColor]]; + } + + NSNumber *strokeWidth = data[@"strokeWidth"]; + if (strokeWidth && strokeWidth != (id)[NSNull null]) { + [self setStrokeWidth:[strokeWidth intValue]]; + } + + NSNumber *fillColor = data[@"fillColor"]; + if (fillColor && fillColor != (id)[NSNull null]) { + [self setFillColor:[FLTGoogleMapJSONConversions colorFromRGBA:fillColor]]; + } +} + +@end + +@interface FLTCirclesController () + +@property(strong, nonatomic) FlutterMethodChannel *methodChannel; +@property(weak, nonatomic) GMSMapView *mapView; +@property(strong, nonatomic) NSMutableDictionary *circleIdToController; + +@end + +@implementation FLTCirclesController + +- (instancetype)init:(FlutterMethodChannel *)methodChannel + mapView:(GMSMapView *)mapView + registrar:(NSObject *)registrar { + self = [super init]; + if (self) { + _methodChannel = methodChannel; + _mapView = mapView; + _circleIdToController = [NSMutableDictionary dictionaryWithCapacity:1]; + } + return self; +} + +- (void)addCircles:(NSArray *)circlesToAdd { + for (NSDictionary *circle in circlesToAdd) { + CLLocationCoordinate2D position = [FLTCirclesController getPosition:circle]; + CLLocationDistance radius = [FLTCirclesController getRadius:circle]; + NSString *circleId = [FLTCirclesController getCircleId:circle]; + FLTGoogleMapCircleController *controller = + [[FLTGoogleMapCircleController alloc] initCircleWithPosition:position + radius:radius + circleId:circleId + mapView:self.mapView + options:circle]; + self.circleIdToController[circleId] = controller; + } +} + +- (void)changeCircles:(NSArray *)circlesToChange { + for (NSDictionary *circle in circlesToChange) { + NSString *circleId = [FLTCirclesController getCircleId:circle]; + FLTGoogleMapCircleController *controller = self.circleIdToController[circleId]; + if (!controller) { + continue; + } + [controller interpretCircleOptions:circle]; + } +} + +- (void)removeCircleWithIdentifiers:(NSArray *)identifiers { + for (NSString *identifier in identifiers) { + FLTGoogleMapCircleController *controller = self.circleIdToController[identifier]; + if (!controller) { + continue; + } + [controller removeCircle]; + [self.circleIdToController removeObjectForKey:identifier]; + } +} + +- (bool)hasCircleWithIdentifier:(NSString *)identifier { + if (!identifier) { + return false; + } + return self.circleIdToController[identifier] != nil; +} + +- (void)didTapCircleWithIdentifier:(NSString *)identifier { + if (!identifier) { + return; + } + FLTGoogleMapCircleController *controller = self.circleIdToController[identifier]; + if (!controller) { + return; + } + [self.methodChannel invokeMethod:@"circle#onTap" arguments:@{@"circleId" : identifier}]; +} + ++ (CLLocationCoordinate2D)getPosition:(NSDictionary *)circle { + NSArray *center = circle[@"center"]; + return [FLTGoogleMapJSONConversions locationFromLatLong:center]; +} + ++ (CLLocationDistance)getRadius:(NSDictionary *)circle { + NSNumber *radius = circle[@"radius"]; + return [radius floatValue]; +} + ++ (NSString *)getCircleId:(NSDictionary *)circle { + return circle[@"circleId"]; +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.h similarity index 51% rename from packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.h rename to packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.h index a8cebb983347..d1069ac16b39 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.h @@ -11,34 +11,13 @@ NS_ASSUME_NONNULL_BEGIN -// Defines map UI options writable from Flutter. -@protocol FLTGoogleMapOptionsSink -- (void)setCameraTargetBounds:(nullable GMSCoordinateBounds *)bounds; -- (void)setCompassEnabled:(BOOL)enabled; -- (void)setIndoorEnabled:(BOOL)enabled; -- (void)setTrafficEnabled:(BOOL)enabled; -- (void)setBuildingsEnabled:(BOOL)enabled; -- (void)setMapType:(GMSMapViewType)type; -- (void)setMinZoom:(float)minZoom maxZoom:(float)maxZoom; -- (void)setPaddingTop:(float)top left:(float)left bottom:(float)bottom right:(float)right; -- (void)setRotateGesturesEnabled:(BOOL)enabled; -- (void)setScrollGesturesEnabled:(BOOL)enabled; -- (void)setTiltGesturesEnabled:(BOOL)enabled; -- (void)setTrackCameraPosition:(BOOL)enabled; -- (void)setZoomGesturesEnabled:(BOOL)enabled; -- (void)setMyLocationEnabled:(BOOL)enabled; -- (void)setMyLocationButtonEnabled:(BOOL)enabled; -- (nullable NSString *)setMapStyle:(NSString *)mapStyle; -@end - // Defines map overlay controllable from Flutter. -@interface FLTGoogleMapController - : NSObject +@interface FLTGoogleMapController : NSObject - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(nullable id)args registrar:(NSObject *)registrar; -- (void)showAtX:(CGFloat)x Y:(CGFloat)y; +- (void)showAtOrigin:(CGPoint)origin; - (void)hide; - (void)animateWithCameraUpdate:(GMSCameraUpdate *)cameraUpdate; - (void)moveWithCameraUpdate:(GMSCameraUpdate *)cameraUpdate; diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m similarity index 54% rename from packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m rename to packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m index df4e8761e6b2..bd50c2d7a6de 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m @@ -3,25 +3,21 @@ // found in the LICENSE file. #import "GoogleMapController.h" +#import "FLTGoogleMapJSONConversions.h" #import "FLTGoogleMapTileOverlayController.h" -#import "JsonConversions.h" #pragma mark - Conversion of JSON-like values sent via platform channels. Forward declarations. -static NSDictionary *PositionToJson(GMSCameraPosition *position); -static NSDictionary *PointToJson(CGPoint point); -static NSArray *LocationToJson(CLLocationCoordinate2D position); -static CGPoint ToCGPoint(NSDictionary *json); -static GMSCameraPosition *ToOptionalCameraPosition(NSDictionary *json); -static GMSCoordinateBounds *ToOptionalBounds(NSArray *json); -static GMSCameraUpdate *ToCameraUpdate(NSArray *data); -static NSDictionary *GMSCoordinateBoundsToJson(GMSCoordinateBounds *bounds); -static void InterpretMapOptions(NSDictionary *data, id sink); -static double ToDouble(NSNumber *data) { return [FLTGoogleMapJsonConversions toDouble:data]; } +@interface FLTGoogleMapFactory () -@implementation FLTGoogleMapFactory { - NSObject *_registrar; -} +@property(weak, nonatomic) NSObject *registrar; +@property(strong, nonatomic, readonly) id sharedMapServices; + +@end + +@implementation FLTGoogleMapFactory + +@synthesize sharedMapServices = _sharedMapServices; - (instancetype)initWithRegistrar:(NSObject *)registrar { self = [super init]; @@ -38,41 +34,67 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar - (NSObject *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args { + // Precache shared map services, if needed. + // Retain the shared map services singleton, don't use the result for anything. + (void)[self sharedMapServices]; + return [[FLTGoogleMapController alloc] initWithFrame:frame viewIdentifier:viewId arguments:args - registrar:_registrar]; + registrar:self.registrar]; } -@end -@implementation FLTGoogleMapController { - GMSMapView *_mapView; - int64_t _viewId; - FlutterMethodChannel *_channel; - BOOL _trackCameraPosition; - NSObject *_registrar; - BOOL _cameraDidInitialSetup; - FLTMarkersController *_markersController; - FLTPolygonsController *_polygonsController; - FLTPolylinesController *_polylinesController; - FLTCirclesController *_circlesController; - FLTTileOverlaysController *_tileOverlaysController; +- (id)sharedMapServices { + if (_sharedMapServices == nil) { + // Calling this prepares GMSServices on a background thread controlled + // by the GoogleMaps framework. + // Retain the singleton to cache the initialization work across all map views. + _sharedMapServices = [GMSServices sharedServices]; + } + return _sharedMapServices; } +@end + +@interface FLTGoogleMapController () + +@property(nonatomic, strong) GMSMapView *mapView; +@property(nonatomic, strong) FlutterMethodChannel *channel; +@property(nonatomic, assign) BOOL trackCameraPosition; +@property(nonatomic, weak) NSObject *registrar; +@property(nonatomic, strong) FLTMarkersController *markersController; +@property(nonatomic, strong) FLTPolygonsController *polygonsController; +@property(nonatomic, strong) FLTPolylinesController *polylinesController; +@property(nonatomic, strong) FLTCirclesController *circlesController; +@property(nonatomic, strong) FLTTileOverlaysController *tileOverlaysController; + +@end + +@implementation FLTGoogleMapController + - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args registrar:(NSObject *)registrar { + GMSCameraPosition *camera = + [FLTGoogleMapJSONConversions cameraPostionFromDictionary:args[@"initialCameraPosition"]]; + GMSMapView *mapView = [GMSMapView mapWithFrame:frame camera:camera]; + return [self initWithMapView:mapView viewIdentifier:viewId arguments:args registrar:registrar]; +} + +- (instancetype)initWithMapView:(GMSMapView *_Nonnull)mapView + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + registrar:(NSObject *_Nonnull)registrar { if (self = [super init]) { - _viewId = viewId; + _mapView = mapView; - GMSCameraPosition *camera = ToOptionalCameraPosition(args[@"initialCameraPosition"]); - _mapView = [GMSMapView mapWithFrame:frame camera:camera]; _mapView.accessibilityElementsHidden = NO; - _trackCameraPosition = NO; - InterpretMapOptions(args[@"options"], self); + // TODO(cyanglaz): avoid sending message to self in the middle of the init method. + // https://github.com/flutter/flutter/issues/104121 + [self interpretMapOptions:args[@"options"]]; NSString *channelName = - [NSString stringWithFormat:@"plugins.flutter.io/google_maps_%lld", viewId]; + [NSString stringWithFormat:@"plugins.flutter.dev/google_maps_ios_%lld", viewId]; _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:registrar.messenger]; __weak __typeof__(self) weakSelf = self; @@ -82,11 +104,11 @@ - (instancetype)initWithFrame:(CGRect)frame } }]; _mapView.delegate = weakSelf; + _mapView.paddingAdjustmentBehavior = kGMSMapViewPaddingAdjustmentBehaviorNever; _registrar = registrar; - _cameraDidInitialSetup = NO; - _markersController = [[FLTMarkersController alloc] init:_channel - mapView:_mapView - registrar:registrar]; + _markersController = [[FLTMarkersController alloc] initWithMethodChannel:_channel + mapView:_mapView + registrar:registrar]; _polygonsController = [[FLTPolygonsController alloc] init:_channel mapView:_mapView registrar:registrar]; @@ -119,36 +141,32 @@ - (instancetype)initWithFrame:(CGRect)frame if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) { [_tileOverlaysController addTileOverlays:tileOverlaysToAdd]; } + + [_mapView addObserver:self forKeyPath:@"frame" options:0 context:nil]; } return self; } - (UIView *)view { - [_mapView addObserver:self forKeyPath:@"frame" options:0 context:nil]; - return _mapView; + return self.mapView; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if (_cameraDidInitialSetup) { - // We only observe the frame for initial setup. - [_mapView removeObserver:self forKeyPath:@"frame"]; - return; - } - if (object == _mapView && [keyPath isEqualToString:@"frame"]) { - CGRect bounds = _mapView.bounds; + if (object == self.mapView && [keyPath isEqualToString:@"frame"]) { + CGRect bounds = self.mapView.bounds; if (CGRectEqualToRect(bounds, CGRectZero)) { // The workaround is to fix an issue that the camera location is not current when // the size of the map is zero at initialization. - // So We only care about the size of the `_mapView`, ignore the frame changes when the size is - // zero. + // So We only care about the size of the `self.mapView`, ignore the frame changes when the + // size is zero. return; } - _cameraDidInitialSetup = YES; - [_mapView removeObserver:self forKeyPath:@"frame"]; - [_mapView moveCamera:[GMSCameraUpdate setCamera:_mapView.camera]]; + // We only observe the frame for initial setup. + [self.mapView removeObserver:self forKeyPath:@"frame"]; + [self.mapView moveCamera:[GMSCameraUpdate setCamera:self.mapView.camera]]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } @@ -156,46 +174,50 @@ - (void)observeValueForKeyPath:(NSString *)keyPath - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if ([call.method isEqualToString:@"map#show"]) { - [self showAtX:ToDouble(call.arguments[@"x"]) Y:ToDouble(call.arguments[@"y"])]; + [self showAtOrigin:CGPointMake([call.arguments[@"x"] doubleValue], + [call.arguments[@"y"] doubleValue])]; result(nil); } else if ([call.method isEqualToString:@"map#hide"]) { [self hide]; result(nil); } else if ([call.method isEqualToString:@"camera#animate"]) { - [self animateWithCameraUpdate:ToCameraUpdate(call.arguments[@"cameraUpdate"])]; + [self + animateWithCameraUpdate:[FLTGoogleMapJSONConversions + cameraUpdateFromChannelValue:call.arguments[@"cameraUpdate"]]]; result(nil); } else if ([call.method isEqualToString:@"camera#move"]) { - [self moveWithCameraUpdate:ToCameraUpdate(call.arguments[@"cameraUpdate"])]; + [self moveWithCameraUpdate:[FLTGoogleMapJSONConversions + cameraUpdateFromChannelValue:call.arguments[@"cameraUpdate"]]]; result(nil); } else if ([call.method isEqualToString:@"map#update"]) { - InterpretMapOptions(call.arguments[@"options"], self); - result(PositionToJson([self cameraPosition])); + [self interpretMapOptions:call.arguments[@"options"]]; + result([FLTGoogleMapJSONConversions dictionaryFromPosition:[self cameraPosition]]); } else if ([call.method isEqualToString:@"map#getVisibleRegion"]) { - if (_mapView != nil) { - GMSVisibleRegion visibleRegion = _mapView.projection.visibleRegion; + if (self.mapView != nil) { + GMSVisibleRegion visibleRegion = self.mapView.projection.visibleRegion; GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithRegion:visibleRegion]; - - result(GMSCoordinateBoundsToJson(bounds)); + result([FLTGoogleMapJSONConversions dictionaryFromCoordinateBounds:bounds]); } else { result([FlutterError errorWithCode:@"GoogleMap uninitialized" message:@"getVisibleRegion called prior to map initialization" details:nil]); } } else if ([call.method isEqualToString:@"map#getScreenCoordinate"]) { - if (_mapView != nil) { - CLLocationCoordinate2D location = [FLTGoogleMapJsonConversions toLocation:call.arguments]; - CGPoint point = [_mapView.projection pointForCoordinate:location]; - result(PointToJson(point)); + if (self.mapView != nil) { + CLLocationCoordinate2D location = + [FLTGoogleMapJSONConversions locationFromLatLong:call.arguments]; + CGPoint point = [self.mapView.projection pointForCoordinate:location]; + result([FLTGoogleMapJSONConversions dictionaryFromPoint:point]); } else { result([FlutterError errorWithCode:@"GoogleMap uninitialized" message:@"getScreenCoordinate called prior to map initialization" details:nil]); } } else if ([call.method isEqualToString:@"map#getLatLng"]) { - if (_mapView != nil && call.arguments) { - CGPoint point = ToCGPoint(call.arguments); - CLLocationCoordinate2D latlng = [_mapView.projection coordinateForPoint:point]; - result(LocationToJson(latlng)); + if (self.mapView != nil && call.arguments) { + CGPoint point = [FLTGoogleMapJSONConversions pointFromDictionary:call.arguments]; + CLLocationCoordinate2D latlng = [self.mapView.projection coordinateForPoint:point]; + result([FLTGoogleMapJSONConversions arrayFromLocation:latlng]); } else { result([FlutterError errorWithCode:@"GoogleMap uninitialized" message:@"getLatLng called prior to map initialization" @@ -205,14 +227,14 @@ - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { result(nil); } else if ([call.method isEqualToString:@"map#takeSnapshot"]) { if (@available(iOS 10.0, *)) { - if (_mapView != nil) { + if (self.mapView != nil) { UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; format.scale = [[UIScreen mainScreen] scale]; UIGraphicsImageRenderer *renderer = - [[UIGraphicsImageRenderer alloc] initWithSize:_mapView.frame.size format:format]; + [[UIGraphicsImageRenderer alloc] initWithSize:self.mapView.frame.size format:format]; UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) { - [_mapView.layer renderInContext:context.CGContext]; + [self.mapView.layer renderInContext:context.CGContext]; }]; result([FlutterStandardTypedData typedDataWithBytes:UIImagePNGRepresentation(image)]); } else { @@ -227,21 +249,21 @@ - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { } else if ([call.method isEqualToString:@"markers#update"]) { id markersToAdd = call.arguments[@"markersToAdd"]; if ([markersToAdd isKindOfClass:[NSArray class]]) { - [_markersController addMarkers:markersToAdd]; + [self.markersController addMarkers:markersToAdd]; } id markersToChange = call.arguments[@"markersToChange"]; if ([markersToChange isKindOfClass:[NSArray class]]) { - [_markersController changeMarkers:markersToChange]; + [self.markersController changeMarkers:markersToChange]; } id markerIdsToRemove = call.arguments[@"markerIdsToRemove"]; if ([markerIdsToRemove isKindOfClass:[NSArray class]]) { - [_markersController removeMarkerIds:markerIdsToRemove]; + [self.markersController removeMarkersWithIdentifiers:markerIdsToRemove]; } result(nil); } else if ([call.method isEqualToString:@"markers#showInfoWindow"]) { id markerId = call.arguments[@"markerId"]; if ([markerId isKindOfClass:[NSString class]]) { - [_markersController showMarkerInfoWindow:markerId result:result]; + [self.markersController showMarkerInfoWindowWithIdentifier:markerId result:result]; } else { result([FlutterError errorWithCode:@"Invalid markerId" message:@"showInfoWindow called with invalid markerId" @@ -250,7 +272,7 @@ - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { } else if ([call.method isEqualToString:@"markers#hideInfoWindow"]) { id markerId = call.arguments[@"markerId"]; if ([markerId isKindOfClass:[NSString class]]) { - [_markersController hideMarkerInfoWindow:markerId result:result]; + [self.markersController hideMarkerInfoWindowWithIdentifier:markerId result:result]; } else { result([FlutterError errorWithCode:@"Invalid markerId" message:@"hideInfoWindow called with invalid markerId" @@ -259,7 +281,7 @@ - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { } else if ([call.method isEqualToString:@"markers#isInfoWindowShown"]) { id markerId = call.arguments[@"markerId"]; if ([markerId isKindOfClass:[NSString class]]) { - [_markersController isMarkerInfoWindowShown:markerId result:result]; + [self.markersController isInfoWindowShownForMarkerWithIdentifier:markerId result:result]; } else { result([FlutterError errorWithCode:@"Invalid markerId" message:@"isInfoWindowShown called with invalid markerId" @@ -268,97 +290,97 @@ - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { } else if ([call.method isEqualToString:@"polygons#update"]) { id polygonsToAdd = call.arguments[@"polygonsToAdd"]; if ([polygonsToAdd isKindOfClass:[NSArray class]]) { - [_polygonsController addPolygons:polygonsToAdd]; + [self.polygonsController addPolygons:polygonsToAdd]; } id polygonsToChange = call.arguments[@"polygonsToChange"]; if ([polygonsToChange isKindOfClass:[NSArray class]]) { - [_polygonsController changePolygons:polygonsToChange]; + [self.polygonsController changePolygons:polygonsToChange]; } id polygonIdsToRemove = call.arguments[@"polygonIdsToRemove"]; if ([polygonIdsToRemove isKindOfClass:[NSArray class]]) { - [_polygonsController removePolygonIds:polygonIdsToRemove]; + [self.polygonsController removePolygonWithIdentifiers:polygonIdsToRemove]; } result(nil); } else if ([call.method isEqualToString:@"polylines#update"]) { id polylinesToAdd = call.arguments[@"polylinesToAdd"]; if ([polylinesToAdd isKindOfClass:[NSArray class]]) { - [_polylinesController addPolylines:polylinesToAdd]; + [self.polylinesController addPolylines:polylinesToAdd]; } id polylinesToChange = call.arguments[@"polylinesToChange"]; if ([polylinesToChange isKindOfClass:[NSArray class]]) { - [_polylinesController changePolylines:polylinesToChange]; + [self.polylinesController changePolylines:polylinesToChange]; } id polylineIdsToRemove = call.arguments[@"polylineIdsToRemove"]; if ([polylineIdsToRemove isKindOfClass:[NSArray class]]) { - [_polylinesController removePolylineIds:polylineIdsToRemove]; + [self.polylinesController removePolylineWithIdentifiers:polylineIdsToRemove]; } result(nil); } else if ([call.method isEqualToString:@"circles#update"]) { id circlesToAdd = call.arguments[@"circlesToAdd"]; if ([circlesToAdd isKindOfClass:[NSArray class]]) { - [_circlesController addCircles:circlesToAdd]; + [self.circlesController addCircles:circlesToAdd]; } id circlesToChange = call.arguments[@"circlesToChange"]; if ([circlesToChange isKindOfClass:[NSArray class]]) { - [_circlesController changeCircles:circlesToChange]; + [self.circlesController changeCircles:circlesToChange]; } id circleIdsToRemove = call.arguments[@"circleIdsToRemove"]; if ([circleIdsToRemove isKindOfClass:[NSArray class]]) { - [_circlesController removeCircleIds:circleIdsToRemove]; + [self.circlesController removeCircleWithIdentifiers:circleIdsToRemove]; } result(nil); } else if ([call.method isEqualToString:@"tileOverlays#update"]) { id tileOverlaysToAdd = call.arguments[@"tileOverlaysToAdd"]; if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) { - [_tileOverlaysController addTileOverlays:tileOverlaysToAdd]; + [self.tileOverlaysController addTileOverlays:tileOverlaysToAdd]; } id tileOverlaysToChange = call.arguments[@"tileOverlaysToChange"]; if ([tileOverlaysToChange isKindOfClass:[NSArray class]]) { - [_tileOverlaysController changeTileOverlays:tileOverlaysToChange]; + [self.tileOverlaysController changeTileOverlays:tileOverlaysToChange]; } id tileOverlayIdsToRemove = call.arguments[@"tileOverlayIdsToRemove"]; if ([tileOverlayIdsToRemove isKindOfClass:[NSArray class]]) { - [_tileOverlaysController removeTileOverlayIds:tileOverlayIdsToRemove]; + [self.tileOverlaysController removeTileOverlayWithIdentifiers:tileOverlayIdsToRemove]; } result(nil); } else if ([call.method isEqualToString:@"tileOverlays#clearTileCache"]) { id rawTileOverlayId = call.arguments[@"tileOverlayId"]; - [_tileOverlaysController clearTileCache:rawTileOverlayId]; + [self.tileOverlaysController clearTileCacheWithIdentifier:rawTileOverlayId]; result(nil); } else if ([call.method isEqualToString:@"map#isCompassEnabled"]) { - NSNumber *isCompassEnabled = @(_mapView.settings.compassButton); + NSNumber *isCompassEnabled = @(self.mapView.settings.compassButton); result(isCompassEnabled); } else if ([call.method isEqualToString:@"map#isMapToolbarEnabled"]) { NSNumber *isMapToolbarEnabled = @NO; result(isMapToolbarEnabled); } else if ([call.method isEqualToString:@"map#getMinMaxZoomLevels"]) { - NSArray *zoomLevels = @[ @(_mapView.minZoom), @(_mapView.maxZoom) ]; + NSArray *zoomLevels = @[ @(self.mapView.minZoom), @(self.mapView.maxZoom) ]; result(zoomLevels); } else if ([call.method isEqualToString:@"map#getZoomLevel"]) { - result(@(_mapView.camera.zoom)); + result(@(self.mapView.camera.zoom)); } else if ([call.method isEqualToString:@"map#isZoomGesturesEnabled"]) { - NSNumber *isZoomGesturesEnabled = @(_mapView.settings.zoomGestures); + NSNumber *isZoomGesturesEnabled = @(self.mapView.settings.zoomGestures); result(isZoomGesturesEnabled); } else if ([call.method isEqualToString:@"map#isZoomControlsEnabled"]) { NSNumber *isZoomControlsEnabled = @NO; result(isZoomControlsEnabled); } else if ([call.method isEqualToString:@"map#isTiltGesturesEnabled"]) { - NSNumber *isTiltGesturesEnabled = @(_mapView.settings.tiltGestures); + NSNumber *isTiltGesturesEnabled = @(self.mapView.settings.tiltGestures); result(isTiltGesturesEnabled); } else if ([call.method isEqualToString:@"map#isRotateGesturesEnabled"]) { - NSNumber *isRotateGesturesEnabled = @(_mapView.settings.rotateGestures); + NSNumber *isRotateGesturesEnabled = @(self.mapView.settings.rotateGestures); result(isRotateGesturesEnabled); } else if ([call.method isEqualToString:@"map#isScrollGesturesEnabled"]) { - NSNumber *isScrollGesturesEnabled = @(_mapView.settings.scrollGestures); + NSNumber *isScrollGesturesEnabled = @(self.mapView.settings.scrollGestures); result(isScrollGesturesEnabled); } else if ([call.method isEqualToString:@"map#isMyLocationButtonEnabled"]) { - NSNumber *isMyLocationButtonEnabled = @(_mapView.settings.myLocationButton); + NSNumber *isMyLocationButtonEnabled = @(self.mapView.settings.myLocationButton); result(isMyLocationButtonEnabled); } else if ([call.method isEqualToString:@"map#isTrafficEnabled"]) { - NSNumber *isTrafficEnabled = @(_mapView.trafficEnabled); + NSNumber *isTrafficEnabled = @(self.mapView.trafficEnabled); result(isTrafficEnabled); } else if ([call.method isEqualToString:@"map#isBuildingsEnabled"]) { - NSNumber *isBuildingsEnabled = @(_mapView.buildingsEnabled); + NSNumber *isBuildingsEnabled = @(self.mapView.buildingsEnabled); result(isBuildingsEnabled); } else if ([call.method isEqualToString:@"map#setStyle"]) { NSString *mapStyle = [call arguments]; @@ -370,86 +392,84 @@ - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { } } else if ([call.method isEqualToString:@"map#getTileOverlayInfo"]) { NSString *rawTileOverlayId = call.arguments[@"tileOverlayId"]; - result([_tileOverlaysController getTileOverlayInfo:rawTileOverlayId]); + result([self.tileOverlaysController tileOverlayInfoWithIdentifier:rawTileOverlayId]); } else { result(FlutterMethodNotImplemented); } } -- (void)showAtX:(CGFloat)x Y:(CGFloat)y { - _mapView.frame = - CGRectMake(x, y, CGRectGetWidth(_mapView.frame), CGRectGetHeight(_mapView.frame)); - _mapView.hidden = NO; +- (void)showAtOrigin:(CGPoint)origin { + CGRect frame = {origin, self.mapView.frame.size}; + self.mapView.frame = frame; + self.mapView.hidden = NO; } - (void)hide { - _mapView.hidden = YES; + self.mapView.hidden = YES; } - (void)animateWithCameraUpdate:(GMSCameraUpdate *)cameraUpdate { - [_mapView animateWithCameraUpdate:cameraUpdate]; + [self.mapView animateWithCameraUpdate:cameraUpdate]; } - (void)moveWithCameraUpdate:(GMSCameraUpdate *)cameraUpdate { - [_mapView moveCamera:cameraUpdate]; + [self.mapView moveCamera:cameraUpdate]; } - (GMSCameraPosition *)cameraPosition { - if (_trackCameraPosition) { - return _mapView.camera; + if (self.trackCameraPosition) { + return self.mapView.camera; } else { return nil; } } -#pragma mark - FLTGoogleMapOptionsSink methods - - (void)setCamera:(GMSCameraPosition *)camera { - _mapView.camera = camera; + self.mapView.camera = camera; } - (void)setCameraTargetBounds:(GMSCoordinateBounds *)bounds { - _mapView.cameraTargetBounds = bounds; + self.mapView.cameraTargetBounds = bounds; } - (void)setCompassEnabled:(BOOL)enabled { - _mapView.settings.compassButton = enabled; + self.mapView.settings.compassButton = enabled; } - (void)setIndoorEnabled:(BOOL)enabled { - _mapView.indoorEnabled = enabled; + self.mapView.indoorEnabled = enabled; } - (void)setTrafficEnabled:(BOOL)enabled { - _mapView.trafficEnabled = enabled; + self.mapView.trafficEnabled = enabled; } - (void)setBuildingsEnabled:(BOOL)enabled { - _mapView.buildingsEnabled = enabled; + self.mapView.buildingsEnabled = enabled; } - (void)setMapType:(GMSMapViewType)mapType { - _mapView.mapType = mapType; + self.mapView.mapType = mapType; } - (void)setMinZoom:(float)minZoom maxZoom:(float)maxZoom { - [_mapView setMinZoom:minZoom maxZoom:maxZoom]; + [self.mapView setMinZoom:minZoom maxZoom:maxZoom]; } - (void)setPaddingTop:(float)top left:(float)left bottom:(float)bottom right:(float)right { - _mapView.padding = UIEdgeInsetsMake(top, left, bottom, right); + self.mapView.padding = UIEdgeInsetsMake(top, left, bottom, right); } - (void)setRotateGesturesEnabled:(BOOL)enabled { - _mapView.settings.rotateGestures = enabled; + self.mapView.settings.rotateGestures = enabled; } - (void)setScrollGesturesEnabled:(BOOL)enabled { - _mapView.settings.scrollGestures = enabled; + self.mapView.settings.scrollGestures = enabled; } - (void)setTiltGesturesEnabled:(BOOL)enabled { - _mapView.settings.tiltGestures = enabled; + self.mapView.settings.tiltGestures = enabled; } - (void)setTrackCameraPosition:(BOOL)enabled { @@ -457,20 +477,20 @@ - (void)setTrackCameraPosition:(BOOL)enabled { } - (void)setZoomGesturesEnabled:(BOOL)enabled { - _mapView.settings.zoomGestures = enabled; + self.mapView.settings.zoomGestures = enabled; } - (void)setMyLocationEnabled:(BOOL)enabled { - _mapView.myLocationEnabled = enabled; + self.mapView.myLocationEnabled = enabled; } - (void)setMyLocationButtonEnabled:(BOOL)enabled { - _mapView.settings.myLocationButton = enabled; + self.mapView.settings.myLocationButton = enabled; } - (NSString *)setMapStyle:(NSString *)mapStyle { if (mapStyle == (id)[NSNull null] || mapStyle.length == 0) { - _mapView.mapStyle = nil; + self.mapView.mapStyle = nil; return nil; } NSError *error; @@ -478,7 +498,7 @@ - (NSString *)setMapStyle:(NSString *)mapStyle { if (!style) { return [error localizedDescription]; } else { - _mapView.mapStyle = style; + self.mapView.mapStyle = style; return nil; } } @@ -486,236 +506,141 @@ - (NSString *)setMapStyle:(NSString *)mapStyle { #pragma mark - GMSMapViewDelegate methods - (void)mapView:(GMSMapView *)mapView willMove:(BOOL)gesture { - [_channel invokeMethod:@"camera#onMoveStarted" arguments:@{@"isGesture" : @(gesture)}]; + [self.channel invokeMethod:@"camera#onMoveStarted" arguments:@{@"isGesture" : @(gesture)}]; } - (void)mapView:(GMSMapView *)mapView didChangeCameraPosition:(GMSCameraPosition *)position { - if (_trackCameraPosition) { - [_channel invokeMethod:@"camera#onMove" arguments:@{@"position" : PositionToJson(position)}]; + if (self.trackCameraPosition) { + [self.channel invokeMethod:@"camera#onMove" + arguments:@{ + @"position" : [FLTGoogleMapJSONConversions dictionaryFromPosition:position] + }]; } } - (void)mapView:(GMSMapView *)mapView idleAtCameraPosition:(GMSCameraPosition *)position { - [_channel invokeMethod:@"camera#onIdle" arguments:@{}]; + [self.channel invokeMethod:@"camera#onIdle" arguments:@{}]; } - (BOOL)mapView:(GMSMapView *)mapView didTapMarker:(GMSMarker *)marker { NSString *markerId = marker.userData[0]; - return [_markersController onMarkerTap:markerId]; + return [self.markersController didTapMarkerWithIdentifier:markerId]; } - (void)mapView:(GMSMapView *)mapView didEndDraggingMarker:(GMSMarker *)marker { NSString *markerId = marker.userData[0]; - [_markersController onMarkerDragEnd:markerId coordinate:marker.position]; + [self.markersController didEndDraggingMarkerWithIdentifier:markerId location:marker.position]; } - (void)mapView:(GMSMapView *)mapView didStartDraggingMarker:(GMSMarker *)marker { NSString *markerId = marker.userData[0]; - [_markersController onMarkerDragStart:markerId coordinate:marker.position]; + [self.markersController didStartDraggingMarkerWithIdentifier:markerId location:marker.position]; } - (void)mapView:(GMSMapView *)mapView didDragMarker:(GMSMarker *)marker { NSString *markerId = marker.userData[0]; - [_markersController onMarkerDrag:markerId coordinate:marker.position]; + [self.markersController didDragMarkerWithIdentifier:markerId location:marker.position]; } - (void)mapView:(GMSMapView *)mapView didTapInfoWindowOfMarker:(GMSMarker *)marker { NSString *markerId = marker.userData[0]; - [_markersController onInfoWindowTap:markerId]; + [self.markersController didTapInfoWindowOfMarkerWithIdentifier:markerId]; } - (void)mapView:(GMSMapView *)mapView didTapOverlay:(GMSOverlay *)overlay { NSString *overlayId = overlay.userData[0]; - if ([_polylinesController hasPolylineWithId:overlayId]) { - [_polylinesController onPolylineTap:overlayId]; - } else if ([_polygonsController hasPolygonWithId:overlayId]) { - [_polygonsController onPolygonTap:overlayId]; - } else if ([_circlesController hasCircleWithId:overlayId]) { - [_circlesController onCircleTap:overlayId]; + if ([self.polylinesController hasPolylineWithIdentifier:overlayId]) { + [self.polylinesController didTapPolylineWithIdentifier:overlayId]; + } else if ([self.polygonsController hasPolygonWithIdentifier:overlayId]) { + [self.polygonsController didTapPolygonWithIdentifier:overlayId]; + } else if ([self.circlesController hasCircleWithIdentifier:overlayId]) { + [self.circlesController didTapCircleWithIdentifier:overlayId]; } } - (void)mapView:(GMSMapView *)mapView didTapAtCoordinate:(CLLocationCoordinate2D)coordinate { - [_channel invokeMethod:@"map#onTap" arguments:@{@"position" : LocationToJson(coordinate)}]; + [self.channel + invokeMethod:@"map#onTap" + arguments:@{@"position" : [FLTGoogleMapJSONConversions arrayFromLocation:coordinate]}]; } - (void)mapView:(GMSMapView *)mapView didLongPressAtCoordinate:(CLLocationCoordinate2D)coordinate { - [_channel invokeMethod:@"map#onLongPress" arguments:@{@"position" : LocationToJson(coordinate)}]; -} - -@end - -#pragma mark - Implementations of JSON conversion functions. - -static NSArray *LocationToJson(CLLocationCoordinate2D position) { - return @[ @(position.latitude), @(position.longitude) ]; + [self.channel + invokeMethod:@"map#onLongPress" + arguments:@{@"position" : [FLTGoogleMapJSONConversions arrayFromLocation:coordinate]}]; } -static NSDictionary *PositionToJson(GMSCameraPosition *position) { - if (!position) { - return nil; - } - return @{ - @"target" : LocationToJson([position target]), - @"zoom" : @([position zoom]), - @"bearing" : @([position bearing]), - @"tilt" : @([position viewingAngle]), - }; -} - -static NSDictionary *PointToJson(CGPoint point) { - return @{ - @"x" : @(lroundf(point.x)), - @"y" : @(lroundf(point.y)), - }; -} - -static NSDictionary *GMSCoordinateBoundsToJson(GMSCoordinateBounds *bounds) { - if (!bounds) { - return nil; - } - return @{ - @"southwest" : LocationToJson([bounds southWest]), - @"northeast" : LocationToJson([bounds northEast]), - }; -} - -static float ToFloat(NSNumber *data) { return [FLTGoogleMapJsonConversions toFloat:data]; } - -static CLLocationCoordinate2D ToLocation(NSArray *data) { - return [FLTGoogleMapJsonConversions toLocation:data]; -} - -static int ToInt(NSNumber *data) { return [FLTGoogleMapJsonConversions toInt:data]; } - -static BOOL ToBool(NSNumber *data) { return [FLTGoogleMapJsonConversions toBool:data]; } - -static CGPoint ToPoint(NSArray *data) { return [FLTGoogleMapJsonConversions toPoint:data]; } - -static GMSCameraPosition *ToCameraPosition(NSDictionary *data) { - return [GMSCameraPosition cameraWithTarget:ToLocation(data[@"target"]) - zoom:ToFloat(data[@"zoom"]) - bearing:ToDouble(data[@"bearing"]) - viewingAngle:ToDouble(data[@"tilt"])]; -} - -static GMSCameraPosition *ToOptionalCameraPosition(NSDictionary *json) { - return json ? ToCameraPosition(json) : nil; -} - -static CGPoint ToCGPoint(NSDictionary *json) { - double x = ToDouble(json[@"x"]); - double y = ToDouble(json[@"y"]); - return CGPointMake(x, y); -} - -static GMSCoordinateBounds *ToBounds(NSArray *data) { - return [[GMSCoordinateBounds alloc] initWithCoordinate:ToLocation(data[0]) - coordinate:ToLocation(data[1])]; -} - -static GMSCoordinateBounds *ToOptionalBounds(NSArray *data) { - return (data[0] == [NSNull null]) ? nil : ToBounds(data[0]); -} - -static GMSMapViewType ToMapViewType(NSNumber *json) { - int value = ToInt(json); - return (GMSMapViewType)(value == 0 ? 5 : value); -} - -static GMSCameraUpdate *ToCameraUpdate(NSArray *data) { - NSString *update = data[0]; - if ([update isEqualToString:@"newCameraPosition"]) { - return [GMSCameraUpdate setCamera:ToCameraPosition(data[1])]; - } else if ([update isEqualToString:@"newLatLng"]) { - return [GMSCameraUpdate setTarget:ToLocation(data[1])]; - } else if ([update isEqualToString:@"newLatLngBounds"]) { - return [GMSCameraUpdate fitBounds:ToBounds(data[1]) withPadding:ToDouble(data[2])]; - } else if ([update isEqualToString:@"newLatLngZoom"]) { - return [GMSCameraUpdate setTarget:ToLocation(data[1]) zoom:ToFloat(data[2])]; - } else if ([update isEqualToString:@"scrollBy"]) { - return [GMSCameraUpdate scrollByX:ToDouble(data[1]) Y:ToDouble(data[2])]; - } else if ([update isEqualToString:@"zoomBy"]) { - if (data.count == 2) { - return [GMSCameraUpdate zoomBy:ToFloat(data[1])]; - } else { - return [GMSCameraUpdate zoomBy:ToFloat(data[1]) atPoint:ToPoint(data[2])]; - } - } else if ([update isEqualToString:@"zoomIn"]) { - return [GMSCameraUpdate zoomIn]; - } else if ([update isEqualToString:@"zoomOut"]) { - return [GMSCameraUpdate zoomOut]; - } else if ([update isEqualToString:@"zoomTo"]) { - return [GMSCameraUpdate zoomTo:ToFloat(data[1])]; - } - return nil; -} - -static void InterpretMapOptions(NSDictionary *data, id sink) { +- (void)interpretMapOptions:(NSDictionary *)data { NSArray *cameraTargetBounds = data[@"cameraTargetBounds"]; - if (cameraTargetBounds) { - [sink setCameraTargetBounds:ToOptionalBounds(cameraTargetBounds)]; + if (cameraTargetBounds && cameraTargetBounds != (id)[NSNull null]) { + [self + setCameraTargetBounds:cameraTargetBounds.count > 0 && cameraTargetBounds[0] != [NSNull null] + ? [FLTGoogleMapJSONConversions + coordinateBoundsFromLatLongs:cameraTargetBounds.firstObject] + : nil]; } NSNumber *compassEnabled = data[@"compassEnabled"]; - if (compassEnabled != nil) { - [sink setCompassEnabled:ToBool(compassEnabled)]; + if (compassEnabled && compassEnabled != (id)[NSNull null]) { + [self setCompassEnabled:[compassEnabled boolValue]]; } id indoorEnabled = data[@"indoorEnabled"]; - if (indoorEnabled) { - [sink setIndoorEnabled:ToBool(indoorEnabled)]; + if (indoorEnabled && indoorEnabled != [NSNull null]) { + [self setIndoorEnabled:[indoorEnabled boolValue]]; } id trafficEnabled = data[@"trafficEnabled"]; - if (trafficEnabled) { - [sink setTrafficEnabled:ToBool(trafficEnabled)]; + if (trafficEnabled && trafficEnabled != [NSNull null]) { + [self setTrafficEnabled:[trafficEnabled boolValue]]; } id buildingsEnabled = data[@"buildingsEnabled"]; - if (buildingsEnabled) { - [sink setBuildingsEnabled:ToBool(buildingsEnabled)]; + if (buildingsEnabled && buildingsEnabled != [NSNull null]) { + [self setBuildingsEnabled:[buildingsEnabled boolValue]]; } id mapType = data[@"mapType"]; - if (mapType) { - [sink setMapType:ToMapViewType(mapType)]; + if (mapType && mapType != [NSNull null]) { + [self setMapType:[FLTGoogleMapJSONConversions mapViewTypeFromTypeValue:mapType]]; } NSArray *zoomData = data[@"minMaxZoomPreference"]; - if (zoomData) { - float minZoom = (zoomData[0] == [NSNull null]) ? kGMSMinZoomLevel : ToFloat(zoomData[0]); - float maxZoom = (zoomData[1] == [NSNull null]) ? kGMSMaxZoomLevel : ToFloat(zoomData[1]); - [sink setMinZoom:minZoom maxZoom:maxZoom]; + if (zoomData && zoomData != (id)[NSNull null]) { + float minZoom = (zoomData[0] == [NSNull null]) ? kGMSMinZoomLevel : [zoomData[0] floatValue]; + float maxZoom = (zoomData[1] == [NSNull null]) ? kGMSMaxZoomLevel : [zoomData[1] floatValue]; + [self setMinZoom:minZoom maxZoom:maxZoom]; } NSArray *paddingData = data[@"padding"]; if (paddingData) { - float top = (paddingData[0] == [NSNull null]) ? 0 : ToFloat(paddingData[0]); - float left = (paddingData[1] == [NSNull null]) ? 0 : ToFloat(paddingData[1]); - float bottom = (paddingData[2] == [NSNull null]) ? 0 : ToFloat(paddingData[2]); - float right = (paddingData[3] == [NSNull null]) ? 0 : ToFloat(paddingData[3]); - [sink setPaddingTop:top left:left bottom:bottom right:right]; + float top = (paddingData[0] == [NSNull null]) ? 0 : [paddingData[0] floatValue]; + float left = (paddingData[1] == [NSNull null]) ? 0 : [paddingData[1] floatValue]; + float bottom = (paddingData[2] == [NSNull null]) ? 0 : [paddingData[2] floatValue]; + float right = (paddingData[3] == [NSNull null]) ? 0 : [paddingData[3] floatValue]; + [self setPaddingTop:top left:left bottom:bottom right:right]; } NSNumber *rotateGesturesEnabled = data[@"rotateGesturesEnabled"]; - if (rotateGesturesEnabled != nil) { - [sink setRotateGesturesEnabled:ToBool(rotateGesturesEnabled)]; + if (rotateGesturesEnabled && rotateGesturesEnabled != (id)[NSNull null]) { + [self setRotateGesturesEnabled:[rotateGesturesEnabled boolValue]]; } NSNumber *scrollGesturesEnabled = data[@"scrollGesturesEnabled"]; - if (scrollGesturesEnabled != nil) { - [sink setScrollGesturesEnabled:ToBool(scrollGesturesEnabled)]; + if (scrollGesturesEnabled && scrollGesturesEnabled != (id)[NSNull null]) { + [self setScrollGesturesEnabled:[scrollGesturesEnabled boolValue]]; } NSNumber *tiltGesturesEnabled = data[@"tiltGesturesEnabled"]; - if (tiltGesturesEnabled != nil) { - [sink setTiltGesturesEnabled:ToBool(tiltGesturesEnabled)]; + if (tiltGesturesEnabled && tiltGesturesEnabled != (id)[NSNull null]) { + [self setTiltGesturesEnabled:[tiltGesturesEnabled boolValue]]; } NSNumber *trackCameraPosition = data[@"trackCameraPosition"]; - if (trackCameraPosition != nil) { - [sink setTrackCameraPosition:ToBool(trackCameraPosition)]; + if (trackCameraPosition && trackCameraPosition != (id)[NSNull null]) { + [self setTrackCameraPosition:[trackCameraPosition boolValue]]; } NSNumber *zoomGesturesEnabled = data[@"zoomGesturesEnabled"]; - if (zoomGesturesEnabled != nil) { - [sink setZoomGesturesEnabled:ToBool(zoomGesturesEnabled)]; + if (zoomGesturesEnabled && zoomGesturesEnabled != (id)[NSNull null]) { + [self setZoomGesturesEnabled:[zoomGesturesEnabled boolValue]]; } NSNumber *myLocationEnabled = data[@"myLocationEnabled"]; - if (myLocationEnabled != nil) { - [sink setMyLocationEnabled:ToBool(myLocationEnabled)]; + if (myLocationEnabled && myLocationEnabled != (id)[NSNull null]) { + [self setMyLocationEnabled:[myLocationEnabled boolValue]]; } NSNumber *myLocationButtonEnabled = data[@"myLocationButtonEnabled"]; - if (myLocationButtonEnabled != nil) { - [sink setMyLocationButtonEnabled:ToBool(myLocationButtonEnabled)]; + if (myLocationButtonEnabled && myLocationButtonEnabled != (id)[NSNull null]) { + [self setMyLocationButtonEnabled:[myLocationButtonEnabled boolValue]]; } } + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController_Test.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController_Test.h new file mode 100644 index 000000000000..84f6f7ca485f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController_Test.h @@ -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 +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTGoogleMapController (Test) + +/** + * Initializes a map controller with a concrete map view. + * + * @param mapView A map view that will be displayed by the controller + * @param viewId A unique identifier for the controller. + * @param args Parameters for initialising the map view. + * @param registrar The plugin registrar passed from Flutter. + */ +- (instancetype)initWithMapView:(GMSMapView *)mapView + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + registrar:(NSObject *)registrar; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.h new file mode 100644 index 000000000000..a33d48073dd2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.h @@ -0,0 +1,44 @@ +// 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 "GoogleMapController.h" + +NS_ASSUME_NONNULL_BEGIN + +// Defines marker controllable by Flutter. +@interface FLTGoogleMapMarkerController : NSObject +@property(assign, nonatomic, readonly) BOOL consumeTapEvents; +- (instancetype)initMarkerWithPosition:(CLLocationCoordinate2D)position + identifier:(NSString *)identifier + mapView:(GMSMapView *)mapView; +- (void)showInfoWindow; +- (void)hideInfoWindow; +- (BOOL)isInfoWindowShown; +- (void)removeMarker; +@end + +@interface FLTMarkersController : NSObject +- (instancetype)initWithMethodChannel:(FlutterMethodChannel *)methodChannel + mapView:(GMSMapView *)mapView + registrar:(NSObject *)registrar; +- (void)addMarkers:(NSArray *)markersToAdd; +- (void)changeMarkers:(NSArray *)markersToChange; +- (void)removeMarkersWithIdentifiers:(NSArray *)identifiers; +- (BOOL)didTapMarkerWithIdentifier:(NSString *)identifier; +- (void)didStartDraggingMarkerWithIdentifier:(NSString *)identifier + location:(CLLocationCoordinate2D)coordinate; +- (void)didEndDraggingMarkerWithIdentifier:(NSString *)identifier + location:(CLLocationCoordinate2D)coordinate; +- (void)didDragMarkerWithIdentifier:(NSString *)identifier + location:(CLLocationCoordinate2D)coordinate; +- (void)didTapInfoWindowOfMarkerWithIdentifier:(NSString *)identifier; +- (void)showMarkerInfoWindowWithIdentifier:(NSString *)identifier result:(FlutterResult)result; +- (void)hideMarkerInfoWindowWithIdentifier:(NSString *)identifier result:(FlutterResult)result; +- (void)isInfoWindowShownForMarkerWithIdentifier:(NSString *)identifier + result:(FlutterResult)result; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.m new file mode 100644 index 000000000000..dd07e791a888 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.m @@ -0,0 +1,387 @@ +// 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 "GoogleMapMarkerController.h" +#import "FLTGoogleMapJSONConversions.h" + +@interface FLTGoogleMapMarkerController () + +@property(strong, nonatomic) GMSMarker *marker; +@property(weak, nonatomic) GMSMapView *mapView; +@property(assign, nonatomic, readwrite) BOOL consumeTapEvents; + +@end + +@implementation FLTGoogleMapMarkerController + +- (instancetype)initMarkerWithPosition:(CLLocationCoordinate2D)position + identifier:(NSString *)identifier + mapView:(GMSMapView *)mapView { + self = [super init]; + if (self) { + _marker = [GMSMarker markerWithPosition:position]; + _mapView = mapView; + _marker.userData = @[ identifier ]; + } + return self; +} + +- (void)showInfoWindow { + self.mapView.selectedMarker = self.marker; +} + +- (void)hideInfoWindow { + if (self.mapView.selectedMarker == self.marker) { + self.mapView.selectedMarker = nil; + } +} + +- (BOOL)isInfoWindowShown { + return self.mapView.selectedMarker == self.marker; +} + +- (void)removeMarker { + self.marker.map = nil; +} + +- (void)setAlpha:(float)alpha { + self.marker.opacity = alpha; +} + +- (void)setAnchor:(CGPoint)anchor { + self.marker.groundAnchor = anchor; +} + +- (void)setDraggable:(BOOL)draggable { + self.marker.draggable = draggable; +} + +- (void)setFlat:(BOOL)flat { + self.marker.flat = flat; +} + +- (void)setIcon:(UIImage *)icon { + self.marker.icon = icon; +} + +- (void)setInfoWindowAnchor:(CGPoint)anchor { + self.marker.infoWindowAnchor = anchor; +} + +- (void)setInfoWindowTitle:(NSString *)title snippet:(NSString *)snippet { + self.marker.title = title; + self.marker.snippet = snippet; +} + +- (void)setPosition:(CLLocationCoordinate2D)position { + self.marker.position = position; +} + +- (void)setRotation:(CLLocationDegrees)rotation { + self.marker.rotation = rotation; +} + +- (void)setVisible:(BOOL)visible { + self.marker.map = visible ? self.mapView : nil; +} + +- (void)setZIndex:(int)zIndex { + self.marker.zIndex = zIndex; +} + +- (void)interpretMarkerOptions:(NSDictionary *)data + registrar:(NSObject *)registrar { + NSNumber *alpha = data[@"alpha"]; + if (alpha && alpha != (id)[NSNull null]) { + [self setAlpha:[alpha floatValue]]; + } + NSArray *anchor = data[@"anchor"]; + if (anchor && anchor != (id)[NSNull null]) { + [self setAnchor:[FLTGoogleMapJSONConversions pointFromArray:anchor]]; + } + NSNumber *draggable = data[@"draggable"]; + if (draggable && draggable != (id)[NSNull null]) { + [self setDraggable:[draggable boolValue]]; + } + NSArray *icon = data[@"icon"]; + if (icon && icon != (id)[NSNull null]) { + UIImage *image = [self extractIconFromData:icon registrar:registrar]; + [self setIcon:image]; + } + NSNumber *flat = data[@"flat"]; + if (flat && flat != (id)[NSNull null]) { + [self setFlat:[flat boolValue]]; + } + NSNumber *consumeTapEvents = data[@"consumeTapEvents"]; + if (consumeTapEvents && consumeTapEvents != (id)[NSNull null]) { + [self setConsumeTapEvents:[consumeTapEvents boolValue]]; + } + [self interpretInfoWindow:data]; + NSArray *position = data[@"position"]; + if (position && position != (id)[NSNull null]) { + [self setPosition:[FLTGoogleMapJSONConversions locationFromLatLong:position]]; + } + NSNumber *rotation = data[@"rotation"]; + if (rotation && rotation != (id)[NSNull null]) { + [self setRotation:[rotation doubleValue]]; + } + NSNumber *visible = data[@"visible"]; + if (visible && visible != (id)[NSNull null]) { + [self setVisible:[visible boolValue]]; + } + NSNumber *zIndex = data[@"zIndex"]; + if (zIndex && zIndex != (id)[NSNull null]) { + [self setZIndex:[zIndex intValue]]; + } +} + +- (void)interpretInfoWindow:(NSDictionary *)data { + NSDictionary *infoWindow = data[@"infoWindow"]; + if (infoWindow && infoWindow != (id)[NSNull null]) { + NSString *title = infoWindow[@"title"]; + NSString *snippet = infoWindow[@"snippet"]; + if (title && title != (id)[NSNull null]) { + [self setInfoWindowTitle:title snippet:snippet]; + } + NSArray *infoWindowAnchor = infoWindow[@"infoWindowAnchor"]; + if (infoWindowAnchor && infoWindowAnchor != (id)[NSNull null]) { + [self setInfoWindowAnchor:[FLTGoogleMapJSONConversions pointFromArray:infoWindowAnchor]]; + } + } +} + +- (UIImage *)extractIconFromData:(NSArray *)iconData + registrar:(NSObject *)registrar { + UIImage *image; + if ([iconData.firstObject isEqualToString:@"defaultMarker"]) { + CGFloat hue = (iconData.count == 1) ? 0.0f : [iconData[1] doubleValue]; + image = [GMSMarker markerImageWithColor:[UIColor colorWithHue:hue / 360.0 + saturation:1.0 + brightness:0.7 + alpha:1.0]]; + } else if ([iconData.firstObject isEqualToString:@"fromAsset"]) { + if (iconData.count == 2) { + image = [UIImage imageNamed:[registrar lookupKeyForAsset:iconData[1]]]; + } else { + image = [UIImage imageNamed:[registrar lookupKeyForAsset:iconData[1] + fromPackage:iconData[2]]]; + } + } else if ([iconData.firstObject isEqualToString:@"fromAssetImage"]) { + if (iconData.count == 3) { + image = [UIImage imageNamed:[registrar lookupKeyForAsset:iconData[1]]]; + id scaleParam = iconData[2]; + image = [self scaleImage:image by:scaleParam]; + } else { + NSString *error = + [NSString stringWithFormat:@"'fromAssetImage' should have exactly 3 arguments. Got: %lu", + (unsigned long)iconData.count]; + NSException *exception = [NSException exceptionWithName:@"InvalidBitmapDescriptor" + reason:error + userInfo:nil]; + @throw exception; + } + } else if ([iconData[0] isEqualToString:@"fromBytes"]) { + if (iconData.count == 2) { + @try { + FlutterStandardTypedData *byteData = iconData[1]; + CGFloat screenScale = [[UIScreen mainScreen] scale]; + image = [UIImage imageWithData:[byteData data] scale:screenScale]; + } @catch (NSException *exception) { + @throw [NSException exceptionWithName:@"InvalidByteDescriptor" + reason:@"Unable to interpret bytes as a valid image." + userInfo:nil]; + } + } else { + NSString *error = [NSString + stringWithFormat:@"fromBytes should have exactly one argument, the bytes. Got: %lu", + (unsigned long)iconData.count]; + NSException *exception = [NSException exceptionWithName:@"InvalidByteDescriptor" + reason:error + userInfo:nil]; + @throw exception; + } + } + + return image; +} + +- (UIImage *)scaleImage:(UIImage *)image by:(id)scaleParam { + double scale = 1.0; + if ([scaleParam isKindOfClass:[NSNumber class]]) { + scale = [scaleParam doubleValue]; + } + if (fabs(scale - 1) > 1e-3) { + return [UIImage imageWithCGImage:[image CGImage] + scale:(image.scale * scale) + orientation:(image.imageOrientation)]; + } + return image; +} + +@end + +@interface FLTMarkersController () + +@property(strong, nonatomic) NSMutableDictionary *markerIdentifierToController; +@property(strong, nonatomic) FlutterMethodChannel *methodChannel; +@property(weak, nonatomic) NSObject *registrar; +@property(weak, nonatomic) GMSMapView *mapView; + +@end + +@implementation FLTMarkersController + +- (instancetype)initWithMethodChannel:(FlutterMethodChannel *)methodChannel + mapView:(GMSMapView *)mapView + registrar:(NSObject *)registrar { + self = [super init]; + if (self) { + _methodChannel = methodChannel; + _mapView = mapView; + _markerIdentifierToController = [[NSMutableDictionary alloc] init]; + _registrar = registrar; + } + return self; +} + +- (void)addMarkers:(NSArray *)markersToAdd { + for (NSDictionary *marker in markersToAdd) { + CLLocationCoordinate2D position = [FLTMarkersController getPosition:marker]; + NSString *identifier = marker[@"markerId"]; + FLTGoogleMapMarkerController *controller = + [[FLTGoogleMapMarkerController alloc] initMarkerWithPosition:position + identifier:identifier + mapView:self.mapView]; + [controller interpretMarkerOptions:marker registrar:self.registrar]; + self.markerIdentifierToController[identifier] = controller; + } +} + +- (void)changeMarkers:(NSArray *)markersToChange { + for (NSDictionary *marker in markersToChange) { + NSString *identifier = marker[@"markerId"]; + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; + if (!controller) { + continue; + } + [controller interpretMarkerOptions:marker registrar:self.registrar]; + } +} + +- (void)removeMarkersWithIdentifiers:(NSArray *)identifiers { + for (NSString *identifier in identifiers) { + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; + if (!controller) { + continue; + } + [controller removeMarker]; + [self.markerIdentifierToController removeObjectForKey:identifier]; + } +} + +- (BOOL)didTapMarkerWithIdentifier:(NSString *)identifier { + if (!identifier) { + return NO; + } + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; + if (!controller) { + return NO; + } + [self.methodChannel invokeMethod:@"marker#onTap" arguments:@{@"markerId" : identifier}]; + return controller.consumeTapEvents; +} + +- (void)didStartDraggingMarkerWithIdentifier:(NSString *)identifier + location:(CLLocationCoordinate2D)location { + if (!identifier) { + return; + } + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; + if (!controller) { + return; + } + [self.methodChannel invokeMethod:@"marker#onDragStart" + arguments:@{ + @"markerId" : identifier, + @"position" : [FLTGoogleMapJSONConversions arrayFromLocation:location] + }]; +} + +- (void)didDragMarkerWithIdentifier:(NSString *)identifier + location:(CLLocationCoordinate2D)location { + if (!identifier) { + return; + } + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; + if (!controller) { + return; + } + [self.methodChannel invokeMethod:@"marker#onDrag" + arguments:@{ + @"markerId" : identifier, + @"position" : [FLTGoogleMapJSONConversions arrayFromLocation:location] + }]; +} + +- (void)didEndDraggingMarkerWithIdentifier:(NSString *)identifier + location:(CLLocationCoordinate2D)location { + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; + if (!controller) { + return; + } + [self.methodChannel invokeMethod:@"marker#onDragEnd" + arguments:@{ + @"markerId" : identifier, + @"position" : [FLTGoogleMapJSONConversions arrayFromLocation:location] + }]; +} + +- (void)didTapInfoWindowOfMarkerWithIdentifier:(NSString *)identifier { + if (identifier && self.markerIdentifierToController[identifier]) { + [self.methodChannel invokeMethod:@"infoWindow#onTap" arguments:@{@"markerId" : identifier}]; + } +} + +- (void)showMarkerInfoWindowWithIdentifier:(NSString *)identifier result:(FlutterResult)result { + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; + if (controller) { + [controller showInfoWindow]; + result(nil); + } else { + result([FlutterError errorWithCode:@"Invalid markerId" + message:@"showInfoWindow called with invalid markerId" + details:nil]); + } +} + +- (void)hideMarkerInfoWindowWithIdentifier:(NSString *)identifier result:(FlutterResult)result { + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; + if (controller) { + [controller hideInfoWindow]; + result(nil); + } else { + result([FlutterError errorWithCode:@"Invalid markerId" + message:@"hideInfoWindow called with invalid markerId" + details:nil]); + } +} + +- (void)isInfoWindowShownForMarkerWithIdentifier:(NSString *)identifier + result:(FlutterResult)result { + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; + if (controller) { + result(@([controller isInfoWindowShown])); + } else { + result([FlutterError errorWithCode:@"Invalid markerId" + message:@"isInfoWindowShown called with invalid markerId" + details:nil]); + } +} + ++ (CLLocationCoordinate2D)getPosition:(NSDictionary *)marker { + NSArray *position = marker[@"position"]; + return [FLTGoogleMapJSONConversions locationFromLatLong:position]; +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapPolygonController.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapPolygonController.h new file mode 100644 index 000000000000..bd0c9110200e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapPolygonController.h @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +// Defines polygon controllable by Flutter. +@interface FLTGoogleMapPolygonController : NSObject +- (instancetype)initPolygonWithPath:(GMSMutablePath *)path + identifier:(NSString *)identifier + mapView:(GMSMapView *)mapView; +- (void)removePolygon; +@end + +@interface FLTPolygonsController : NSObject +- (instancetype)init:(FlutterMethodChannel *)methodChannel + mapView:(GMSMapView *)mapView + registrar:(NSObject *)registrar; +- (void)addPolygons:(NSArray *)polygonsToAdd; +- (void)changePolygons:(NSArray *)polygonsToChange; +- (void)removePolygonWithIdentifiers:(NSArray *)identifiers; +- (void)didTapPolygonWithIdentifier:(NSString *)identifier; +- (bool)hasPolygonWithIdentifier:(NSString *)identifier; +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapPolygonController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapPolygonController.m new file mode 100644 index 000000000000..398adfcacecb --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapPolygonController.m @@ -0,0 +1,206 @@ +// 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 "GoogleMapPolygonController.h" +#import "FLTGoogleMapJSONConversions.h" + +@interface FLTGoogleMapPolygonController () + +@property(strong, nonatomic) GMSPolygon *polygon; +@property(weak, nonatomic) GMSMapView *mapView; + +@end + +@implementation FLTGoogleMapPolygonController + +- (instancetype)initPolygonWithPath:(GMSMutablePath *)path + identifier:(NSString *)identifier + mapView:(GMSMapView *)mapView { + self = [super init]; + if (self) { + _polygon = [GMSPolygon polygonWithPath:path]; + _mapView = mapView; + _polygon.userData = @[ identifier ]; + } + return self; +} + +- (void)removePolygon { + self.polygon.map = nil; +} + +- (void)setConsumeTapEvents:(BOOL)consumes { + self.polygon.tappable = consumes; +} +- (void)setVisible:(BOOL)visible { + self.polygon.map = visible ? self.mapView : nil; +} +- (void)setZIndex:(int)zIndex { + self.polygon.zIndex = zIndex; +} +- (void)setPoints:(NSArray *)points { + GMSMutablePath *path = [GMSMutablePath path]; + + for (CLLocation *location in points) { + [path addCoordinate:location.coordinate]; + } + self.polygon.path = path; +} +- (void)setHoles:(NSArray *> *)rawHoles { + NSMutableArray *holes = [[NSMutableArray alloc] init]; + + for (NSArray *points in rawHoles) { + GMSMutablePath *path = [GMSMutablePath path]; + for (CLLocation *location in points) { + [path addCoordinate:location.coordinate]; + } + [holes addObject:path]; + } + + self.polygon.holes = holes; +} + +- (void)setFillColor:(UIColor *)color { + self.polygon.fillColor = color; +} +- (void)setStrokeColor:(UIColor *)color { + self.polygon.strokeColor = color; +} +- (void)setStrokeWidth:(CGFloat)width { + self.polygon.strokeWidth = width; +} + +- (void)interpretPolygonOptions:(NSDictionary *)data + registrar:(NSObject *)registrar { + NSNumber *consumeTapEvents = data[@"consumeTapEvents"]; + if (consumeTapEvents && consumeTapEvents != (id)[NSNull null]) { + [self setConsumeTapEvents:[consumeTapEvents boolValue]]; + } + + NSNumber *visible = data[@"visible"]; + if (visible && visible != (id)[NSNull null]) { + [self setVisible:[visible boolValue]]; + } + + NSNumber *zIndex = data[@"zIndex"]; + if (zIndex && zIndex != (id)[NSNull null]) { + [self setZIndex:[zIndex intValue]]; + } + + NSArray *points = data[@"points"]; + if (points && points != (id)[NSNull null]) { + [self setPoints:[FLTGoogleMapJSONConversions pointsFromLatLongs:points]]; + } + + NSArray *holes = data[@"holes"]; + if (holes && holes != (id)[NSNull null]) { + [self setHoles:[FLTGoogleMapJSONConversions holesFromPointsArray:holes]]; + } + + NSNumber *fillColor = data[@"fillColor"]; + if (fillColor && fillColor != (id)[NSNull null]) { + [self setFillColor:[FLTGoogleMapJSONConversions colorFromRGBA:fillColor]]; + } + + NSNumber *strokeColor = data[@"strokeColor"]; + if (strokeColor && strokeColor != (id)[NSNull null]) { + [self setStrokeColor:[FLTGoogleMapJSONConversions colorFromRGBA:strokeColor]]; + } + + NSNumber *strokeWidth = data[@"strokeWidth"]; + if (strokeWidth && strokeWidth != (id)[NSNull null]) { + [self setStrokeWidth:[strokeWidth intValue]]; + } +} + +@end + +@interface FLTPolygonsController () + +@property(strong, nonatomic) NSMutableDictionary *polygonIdentifierToController; +@property(strong, nonatomic) FlutterMethodChannel *methodChannel; +@property(weak, nonatomic) NSObject *registrar; +@property(weak, nonatomic) GMSMapView *mapView; + +@end + +@implementation FLTPolygonsController + +- (instancetype)init:(FlutterMethodChannel *)methodChannel + mapView:(GMSMapView *)mapView + registrar:(NSObject *)registrar { + self = [super init]; + if (self) { + _methodChannel = methodChannel; + _mapView = mapView; + _polygonIdentifierToController = [NSMutableDictionary dictionaryWithCapacity:1]; + _registrar = registrar; + } + return self; +} + +- (void)addPolygons:(NSArray *)polygonsToAdd { + for (NSDictionary *polygon in polygonsToAdd) { + GMSMutablePath *path = [FLTPolygonsController getPath:polygon]; + NSString *identifier = polygon[@"polygonId"]; + FLTGoogleMapPolygonController *controller = + [[FLTGoogleMapPolygonController alloc] initPolygonWithPath:path + identifier:identifier + mapView:self.mapView]; + [controller interpretPolygonOptions:polygon registrar:self.registrar]; + self.polygonIdentifierToController[identifier] = controller; + } +} + +- (void)changePolygons:(NSArray *)polygonsToChange { + for (NSDictionary *polygon in polygonsToChange) { + NSString *identifier = polygon[@"polygonId"]; + FLTGoogleMapPolygonController *controller = self.polygonIdentifierToController[identifier]; + if (!controller) { + continue; + } + [controller interpretPolygonOptions:polygon registrar:self.registrar]; + } +} + +- (void)removePolygonWithIdentifiers:(NSArray *)identifiers { + for (NSString *identifier in identifiers) { + FLTGoogleMapPolygonController *controller = self.polygonIdentifierToController[identifier]; + if (!controller) { + continue; + } + [controller removePolygon]; + [self.polygonIdentifierToController removeObjectForKey:identifier]; + } +} + +- (void)didTapPolygonWithIdentifier:(NSString *)identifier { + if (!identifier) { + return; + } + FLTGoogleMapPolygonController *controller = self.polygonIdentifierToController[identifier]; + if (!controller) { + return; + } + [self.methodChannel invokeMethod:@"polygon#onTap" arguments:@{@"polygonId" : identifier}]; +} + +- (bool)hasPolygonWithIdentifier:(NSString *)identifier { + if (!identifier) { + return false; + } + return self.polygonIdentifierToController[identifier] != nil; +} + ++ (GMSMutablePath *)getPath:(NSDictionary *)polygon { + NSArray *pointArray = polygon[@"points"]; + NSArray *points = [FLTGoogleMapJSONConversions pointsFromLatLongs:pointArray]; + GMSMutablePath *path = [GMSMutablePath path]; + for (CLLocation *location in points) { + [path addCoordinate:location.coordinate]; + } + return path; +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapPolylineController.h similarity index 50% rename from packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.h rename to packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapPolylineController.h index 0e614eeb62ab..f85d1a3896fa 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapPolylineController.h @@ -5,22 +5,10 @@ #import #import -// Defines polyline UI options writable from Flutter. -@protocol FLTGoogleMapPolylineOptionsSink -- (void)setConsumeTapEvents:(BOOL)consume; -- (void)setVisible:(BOOL)visible; -- (void)setColor:(UIColor *)color; -- (void)setStrokeWidth:(CGFloat)width; -- (void)setPoints:(NSArray *)points; -- (void)setZIndex:(int)zIndex; -- (void)setGeodesic:(BOOL)isGeodesic; -@end - // Defines polyline controllable by Flutter. -@interface FLTGoogleMapPolylineController : NSObject -@property(atomic, readonly) NSString *polylineId; +@interface FLTGoogleMapPolylineController : NSObject - (instancetype)initPolylineWithPath:(GMSMutablePath *)path - polylineId:(NSString *)polylineId + identifier:(NSString *)identifier mapView:(GMSMapView *)mapView; - (void)removePolyline; @end @@ -31,7 +19,7 @@ registrar:(NSObject *)registrar; - (void)addPolylines:(NSArray *)polylinesToAdd; - (void)changePolylines:(NSArray *)polylinesToChange; -- (void)removePolylineIds:(NSArray *)polylineIdsToRemove; -- (void)onPolylineTap:(NSString *)polylineId; -- (bool)hasPolylineWithId:(NSString *)polylineId; +- (void)removePolylineWithIdentifiers:(NSArray *)identifiers; +- (void)didTapPolylineWithIdentifier:(NSString *)identifier; +- (bool)hasPolylineWithIdentifier:(NSString *)identifier; @end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapPolylineController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapPolylineController.m new file mode 100644 index 000000000000..77601d4a1bb5 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapPolylineController.m @@ -0,0 +1,184 @@ +// 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 "GoogleMapPolylineController.h" +#import "FLTGoogleMapJSONConversions.h" + +@interface FLTGoogleMapPolylineController () + +@property(strong, nonatomic) GMSPolyline *polyline; +@property(weak, nonatomic) GMSMapView *mapView; + +@end + +@implementation FLTGoogleMapPolylineController + +- (instancetype)initPolylineWithPath:(GMSMutablePath *)path + identifier:(NSString *)identifier + mapView:(GMSMapView *)mapView { + self = [super init]; + if (self) { + _polyline = [GMSPolyline polylineWithPath:path]; + _mapView = mapView; + _polyline.userData = @[ identifier ]; + } + return self; +} + +- (void)removePolyline { + self.polyline.map = nil; +} + +- (void)setConsumeTapEvents:(BOOL)consumes { + self.polyline.tappable = consumes; +} +- (void)setVisible:(BOOL)visible { + self.polyline.map = visible ? self.mapView : nil; +} +- (void)setZIndex:(int)zIndex { + self.polyline.zIndex = zIndex; +} +- (void)setPoints:(NSArray *)points { + GMSMutablePath *path = [GMSMutablePath path]; + + for (CLLocation *location in points) { + [path addCoordinate:location.coordinate]; + } + self.polyline.path = path; +} + +- (void)setColor:(UIColor *)color { + self.polyline.strokeColor = color; +} +- (void)setStrokeWidth:(CGFloat)width { + self.polyline.strokeWidth = width; +} + +- (void)setGeodesic:(BOOL)isGeodesic { + self.polyline.geodesic = isGeodesic; +} + +- (void)interpretPolylineOptions:(NSDictionary *)data + registrar:(NSObject *)registrar { + NSNumber *consumeTapEvents = data[@"consumeTapEvents"]; + if (consumeTapEvents && consumeTapEvents != (id)[NSNull null]) { + [self setConsumeTapEvents:[consumeTapEvents boolValue]]; + } + + NSNumber *visible = data[@"visible"]; + if (visible && visible != (id)[NSNull null]) { + [self setVisible:[visible boolValue]]; + } + + NSNumber *zIndex = data[@"zIndex"]; + if (zIndex && zIndex != (id)[NSNull null]) { + [self setZIndex:[zIndex intValue]]; + } + + NSArray *points = data[@"points"]; + if (points && points != (id)[NSNull null]) { + [self setPoints:[FLTGoogleMapJSONConversions pointsFromLatLongs:points]]; + } + + NSNumber *strokeColor = data[@"color"]; + if (strokeColor && strokeColor != (id)[NSNull null]) { + [self setColor:[FLTGoogleMapJSONConversions colorFromRGBA:strokeColor]]; + } + + NSNumber *strokeWidth = data[@"width"]; + if (strokeWidth && strokeWidth != (id)[NSNull null]) { + [self setStrokeWidth:[strokeWidth intValue]]; + } + + NSNumber *geodesic = data[@"geodesic"]; + if (geodesic && geodesic != (id)[NSNull null]) { + [self setGeodesic:geodesic.boolValue]; + } +} + +@end + +@interface FLTPolylinesController () + +@property(strong, nonatomic) NSMutableDictionary *polylineIdentifierToController; +@property(strong, nonatomic) FlutterMethodChannel *methodChannel; +@property(weak, nonatomic) NSObject *registrar; +@property(weak, nonatomic) GMSMapView *mapView; + +@end +; + +@implementation FLTPolylinesController + +- (instancetype)init:(FlutterMethodChannel *)methodChannel + mapView:(GMSMapView *)mapView + registrar:(NSObject *)registrar { + self = [super init]; + if (self) { + _methodChannel = methodChannel; + _mapView = mapView; + _polylineIdentifierToController = [NSMutableDictionary dictionaryWithCapacity:1]; + _registrar = registrar; + } + return self; +} +- (void)addPolylines:(NSArray *)polylinesToAdd { + for (NSDictionary *polyline in polylinesToAdd) { + GMSMutablePath *path = [FLTPolylinesController getPath:polyline]; + NSString *identifier = polyline[@"polylineId"]; + FLTGoogleMapPolylineController *controller = + [[FLTGoogleMapPolylineController alloc] initPolylineWithPath:path + identifier:identifier + mapView:self.mapView]; + [controller interpretPolylineOptions:polyline registrar:self.registrar]; + self.polylineIdentifierToController[identifier] = controller; + } +} +- (void)changePolylines:(NSArray *)polylinesToChange { + for (NSDictionary *polyline in polylinesToChange) { + NSString *identifier = polyline[@"polylineId"]; + FLTGoogleMapPolylineController *controller = self.polylineIdentifierToController[identifier]; + if (!controller) { + continue; + } + [controller interpretPolylineOptions:polyline registrar:self.registrar]; + } +} +- (void)removePolylineWithIdentifiers:(NSArray *)identifiers { + for (NSString *identifier in identifiers) { + FLTGoogleMapPolylineController *controller = self.polylineIdentifierToController[identifier]; + if (!controller) { + continue; + } + [controller removePolyline]; + [self.polylineIdentifierToController removeObjectForKey:identifier]; + } +} +- (void)didTapPolylineWithIdentifier:(NSString *)identifier { + if (!identifier) { + return; + } + FLTGoogleMapPolylineController *controller = self.polylineIdentifierToController[identifier]; + if (!controller) { + return; + } + [self.methodChannel invokeMethod:@"polyline#onTap" arguments:@{@"polylineId" : identifier}]; +} +- (bool)hasPolylineWithIdentifier:(NSString *)identifier { + if (!identifier) { + return false; + } + return self.polylineIdentifierToController[identifier] != nil; +} ++ (GMSMutablePath *)getPath:(NSDictionary *)polyline { + NSArray *pointArray = polyline[@"points"]; + NSArray *points = [FLTGoogleMapJSONConversions pointsFromLatLongs:pointArray]; + GMSMutablePath *path = [GMSMutablePath path]; + for (CLLocation *location in points) { + [path addCoordinate:location.coordinate]; + } + return path; +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios-umbrella.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios-umbrella.h new file mode 100644 index 000000000000..791c3aaea6c3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios-umbrella.h @@ -0,0 +1,11 @@ +// 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 +#import + +FOUNDATION_EXPORT double google_maps_flutterVersionNumber; +FOUNDATION_EXPORT const unsigned char google_maps_flutterVersionString[]; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios.modulemap b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios.modulemap new file mode 100644 index 000000000000..699e6753db38 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios.modulemap @@ -0,0 +1,10 @@ +framework module google_maps_flutter_ios { + umbrella header "google_maps_flutter_ios-umbrella.h" + + export * + module * { export * } + + explicit module Test { + header "GoogleMapController_Test.h" + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec b/packages/google_maps_flutter/google_maps_flutter_ios/ios/google_maps_flutter_ios.podspec similarity index 86% rename from packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec rename to packages/google_maps_flutter/google_maps_flutter_ios/ios/google_maps_flutter_ios.podspec index f2ed5fc56ea0..14be02f372e4 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/google_maps_flutter_ios.podspec @@ -2,7 +2,7 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| - s.name = 'google_maps_flutter' + s.name = 'google_maps_flutter_ios' s.version = '0.0.1' s.summary = 'Google Maps for Flutter' s.description = <<-DESC @@ -12,10 +12,11 @@ Downloaded by pub (not CocoaPods). s.homepage = 'https://github.com/flutter/plugins' s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter' } - s.documentation_url = 'https://pub.dev/packages/google_maps_flutter' - s.source_files = 'Classes/**/*' + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter/ios' } + s.documentation_url = 'https://pub.dev/packages/google_maps_flutter_ios' + s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' + s.module_map = 'Classes/google_maps_flutter_ios.modulemap' s.dependency 'Flutter' s.dependency 'GoogleMaps' s.static_framework = true diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/google_maps_flutter_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/google_maps_flutter_ios.dart new file mode 100644 index 000000000000..c4aabbb8919f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/google_maps_flutter_ios.dart @@ -0,0 +1,5 @@ +// 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. + +export 'src/google_maps_flutter_ios.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart new file mode 100644 index 000000000000..8fae1a35e316 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart @@ -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. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +/// An Android of implementation of [GoogleMapsInspectorPlatform]. +@visibleForTesting +class GoogleMapsInspectorIOS extends GoogleMapsInspectorPlatform { + /// Creates a method-channel-based inspector instance that gets the channel + /// for a given map ID from [channelProvider]. + GoogleMapsInspectorIOS(MethodChannel? Function(int mapId) channelProvider) + : _channelProvider = channelProvider; + + final MethodChannel? Function(int mapId) _channelProvider; + + @override + Future areBuildingsEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isBuildingsEnabled'))!; + } + + @override + Future areRotateGesturesEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isRotateGesturesEnabled'))!; + } + + @override + Future areScrollGesturesEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isScrollGesturesEnabled'))!; + } + + @override + Future areTiltGesturesEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isTiltGesturesEnabled'))!; + } + + @override + Future areZoomControlsEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isZoomControlsEnabled'))!; + } + + @override + Future areZoomGesturesEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isZoomGesturesEnabled'))!; + } + + @override + Future getMinMaxZoomLevels({required int mapId}) async { + final List zoomLevels = (await _channelProvider(mapId)! + .invokeMethod>('map#getMinMaxZoomLevels'))! + .cast(); + return MinMaxZoomPreference(zoomLevels[0], zoomLevels[1]); + } + + @override + Future getTileOverlayInfo(TileOverlayId tileOverlayId, + {required int mapId}) async { + final Map? tileInfo = await _channelProvider(mapId)! + .invokeMapMethod( + 'map#getTileOverlayInfo', { + 'tileOverlayId': tileOverlayId.value, + }); + if (tileInfo == null) { + return null; + } + return TileOverlay( + tileOverlayId: tileOverlayId, + fadeIn: tileInfo['fadeIn']! as bool, + transparency: tileInfo['transparency']! as double, + visible: tileInfo['visible']! as bool, + // Android and iOS return different types. + zIndex: (tileInfo['zIndex']! as num).toInt(), + ); + } + + @override + Future isCompassEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isCompassEnabled'))!; + } + + @override + Future isLiteModeEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isLiteModeEnabled'))!; + } + + @override + Future isMapToolbarEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isMapToolbarEnabled'))!; + } + + @override + Future isMyLocationButtonEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isMyLocationButtonEnabled'))!; + } + + @override + Future isTrafficEnabled({required int mapId}) async { + return (await _channelProvider(mapId)! + .invokeMethod('map#isTrafficEnabled'))!; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart new file mode 100644 index 000000000000..5298377763aa --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart @@ -0,0 +1,644 @@ +// 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(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:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:stream_transform/stream_transform.dart'; + +import 'google_map_inspector_ios.dart'; + +// TODO(stuartmorgan): Remove the dependency on platform interface toJson +// methods. Channel serialization details should all be package-internal. + +/// Error thrown when an unknown map ID is provided to a method channel API. +class UnknownMapIDError extends Error { + /// Creates an assertion error with the provided [mapId] and optional + /// [message]. + UnknownMapIDError(this.mapId, [this.message]); + + /// The unknown ID. + final int mapId; + + /// Message describing the assertion error. + final Object? message; + + @override + String toString() { + if (message != null) { + return 'Unknown map ID $mapId: ${Error.safeToString(message)}'; + } + return 'Unknown map ID $mapId'; + } +} + +/// An implementation of [GoogleMapsFlutterPlatform] for iOS. +class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { + /// Registers the iOS implementation of GoogleMapsFlutterPlatform. + static void registerWith() { + GoogleMapsFlutterPlatform.instance = GoogleMapsFlutterIOS(); + } + + // Keep a collection of id -> channel + // Every method call passes the int mapId + final Map _channels = {}; + + /// Accesses the MethodChannel associated to the passed mapId. + MethodChannel _channel(int mapId) { + final MethodChannel? channel = _channels[mapId]; + if (channel == null) { + throw UnknownMapIDError(mapId); + } + return channel; + } + + // Keep a collection of mapId to a map of TileOverlays. + final Map> _tileOverlays = + >{}; + + /// Returns the channel for [mapId], creating it if it doesn't already exist. + @visibleForTesting + MethodChannel ensureChannelInitialized(int mapId) { + MethodChannel? channel = _channels[mapId]; + if (channel == null) { + channel = MethodChannel('plugins.flutter.dev/google_maps_ios_$mapId'); + channel.setMethodCallHandler( + (MethodCall call) => _handleMethodCall(call, mapId)); + _channels[mapId] = channel; + } + return channel; + } + + @override + Future init(int mapId) { + final MethodChannel channel = ensureChannelInitialized(mapId); + return channel.invokeMethod('map#waitForMap'); + } + + @override + void dispose({required int mapId}) { + // Noop! + } + + // The controller we need to broadcast the different events coming + // from handleMethodCall. + // + // It is a `broadcast` because multiple controllers will connect to + // different stream views of this Controller. + final StreamController> _mapEventStreamController = + StreamController>.broadcast(); + + // Returns a filtered view of the events in the _controller, by mapId. + Stream> _events(int mapId) => + _mapEventStreamController.stream + .where((MapEvent event) => event.mapId == mapId); + + @override + Stream onCameraMoveStarted({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCameraMove({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCameraIdle({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onInfoWindowTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDragStart({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDrag({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDragEnd({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onPolylineTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onPolygonTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCircleTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onLongPress({required int mapId}) { + return _events(mapId).whereType(); + } + + Future _handleMethodCall(MethodCall call, int mapId) async { + switch (call.method) { + case 'camera#onMoveStarted': + _mapEventStreamController.add(CameraMoveStartedEvent(mapId)); + break; + case 'camera#onMove': + _mapEventStreamController.add(CameraMoveEvent( + mapId, + CameraPosition.fromMap(call.arguments['position'])!, + )); + break; + case 'camera#onIdle': + _mapEventStreamController.add(CameraIdleEvent(mapId)); + break; + case 'marker#onTap': + _mapEventStreamController.add(MarkerTapEvent( + mapId, + MarkerId(call.arguments['markerId'] as String), + )); + break; + case 'marker#onDragStart': + _mapEventStreamController.add(MarkerDragStartEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + MarkerId(call.arguments['markerId'] as String), + )); + break; + case 'marker#onDrag': + _mapEventStreamController.add(MarkerDragEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + MarkerId(call.arguments['markerId'] as String), + )); + break; + case 'marker#onDragEnd': + _mapEventStreamController.add(MarkerDragEndEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + MarkerId(call.arguments['markerId'] as String), + )); + break; + case 'infoWindow#onTap': + _mapEventStreamController.add(InfoWindowTapEvent( + mapId, + MarkerId(call.arguments['markerId'] as String), + )); + break; + case 'polyline#onTap': + _mapEventStreamController.add(PolylineTapEvent( + mapId, + PolylineId(call.arguments['polylineId'] as String), + )); + break; + case 'polygon#onTap': + _mapEventStreamController.add(PolygonTapEvent( + mapId, + PolygonId(call.arguments['polygonId'] as String), + )); + break; + case 'circle#onTap': + _mapEventStreamController.add(CircleTapEvent( + mapId, + CircleId(call.arguments['circleId'] as String), + )); + break; + case 'map#onTap': + _mapEventStreamController.add(MapTapEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + )); + break; + case 'map#onLongPress': + _mapEventStreamController.add(MapLongPressEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + )); + break; + case 'tileOverlay#getTile': + final Map? tileOverlaysForThisMap = + _tileOverlays[mapId]; + final String tileOverlayId = call.arguments['tileOverlayId'] as String; + final TileOverlay? tileOverlay = + tileOverlaysForThisMap?[TileOverlayId(tileOverlayId)]; + final TileProvider? tileProvider = tileOverlay?.tileProvider; + if (tileProvider == null) { + return TileProvider.noTile.toJson(); + } + final Tile tile = await tileProvider.getTile( + call.arguments['x'] as int, + call.arguments['y'] as int, + call.arguments['zoom'] as int?, + ); + return tile.toJson(); + default: + throw MissingPluginException(); + } + } + + @override + Future updateMapOptions( + Map optionsUpdate, { + required int mapId, + }) { + assert(optionsUpdate != null); + return _channel(mapId).invokeMethod( + 'map#update', + { + 'options': optionsUpdate, + }, + ); + } + + @override + Future updateMarkers( + MarkerUpdates markerUpdates, { + required int mapId, + }) { + assert(markerUpdates != null); + return _channel(mapId).invokeMethod( + 'markers#update', + markerUpdates.toJson(), + ); + } + + @override + Future updatePolygons( + PolygonUpdates polygonUpdates, { + required int mapId, + }) { + assert(polygonUpdates != null); + return _channel(mapId).invokeMethod( + 'polygons#update', + polygonUpdates.toJson(), + ); + } + + @override + Future updatePolylines( + PolylineUpdates polylineUpdates, { + required int mapId, + }) { + assert(polylineUpdates != null); + return _channel(mapId).invokeMethod( + 'polylines#update', + polylineUpdates.toJson(), + ); + } + + @override + Future updateCircles( + CircleUpdates circleUpdates, { + required int mapId, + }) { + assert(circleUpdates != null); + return _channel(mapId).invokeMethod( + 'circles#update', + circleUpdates.toJson(), + ); + } + + @override + Future updateTileOverlays({ + required Set newTileOverlays, + required int mapId, + }) { + final Map? currentTileOverlays = + _tileOverlays[mapId]; + final Set previousSet = currentTileOverlays != null + ? currentTileOverlays.values.toSet() + : {}; + final _TileOverlayUpdates updates = + _TileOverlayUpdates.from(previousSet, newTileOverlays); + _tileOverlays[mapId] = keyTileOverlayId(newTileOverlays); + return _channel(mapId).invokeMethod( + 'tileOverlays#update', + updates.toJson(), + ); + } + + @override + Future clearTileCache( + TileOverlayId tileOverlayId, { + required int mapId, + }) { + return _channel(mapId) + .invokeMethod('tileOverlays#clearTileCache', { + 'tileOverlayId': tileOverlayId.value, + }); + } + + @override + Future animateCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) { + return _channel(mapId) + .invokeMethod('camera#animate', { + 'cameraUpdate': cameraUpdate.toJson(), + }); + } + + @override + Future moveCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) { + return _channel(mapId).invokeMethod('camera#move', { + 'cameraUpdate': cameraUpdate.toJson(), + }); + } + + @override + Future setMapStyle( + String? mapStyle, { + required int mapId, + }) async { + final List successAndError = (await _channel(mapId) + .invokeMethod>('map#setStyle', mapStyle))!; + final bool success = successAndError[0] as bool; + if (!success) { + throw MapStyleException(successAndError[1] as String); + } + } + + @override + Future getVisibleRegion({ + required int mapId, + }) async { + final Map latLngBounds = (await _channel(mapId) + .invokeMapMethod('map#getVisibleRegion'))!; + final LatLng southwest = LatLng.fromJson(latLngBounds['southwest'])!; + final LatLng northeast = LatLng.fromJson(latLngBounds['northeast'])!; + + return LatLngBounds(northeast: northeast, southwest: southwest); + } + + @override + Future getScreenCoordinate( + LatLng latLng, { + required int mapId, + }) async { + final Map point = (await _channel(mapId) + .invokeMapMethod( + 'map#getScreenCoordinate', latLng.toJson()))!; + + return ScreenCoordinate(x: point['x']!, y: point['y']!); + } + + @override + Future getLatLng( + ScreenCoordinate screenCoordinate, { + required int mapId, + }) async { + final List latLng = (await _channel(mapId) + .invokeMethod>( + 'map#getLatLng', screenCoordinate.toJson()))!; + return LatLng(latLng[0] as double, latLng[1] as double); + } + + @override + Future showMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) { + assert(markerId != null); + return _channel(mapId).invokeMethod( + 'markers#showInfoWindow', {'markerId': markerId.value}); + } + + @override + Future hideMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) { + assert(markerId != null); + return _channel(mapId).invokeMethod( + 'markers#hideInfoWindow', {'markerId': markerId.value}); + } + + @override + Future isMarkerInfoWindowShown( + MarkerId markerId, { + required int mapId, + }) async { + assert(markerId != null); + return (await _channel(mapId).invokeMethod( + 'markers#isInfoWindowShown', + {'markerId': markerId.value}))!; + } + + @override + Future getZoomLevel({ + required int mapId, + }) async { + return (await _channel(mapId).invokeMethod('map#getZoomLevel'))!; + } + + @override + Future takeSnapshot({ + required int mapId, + }) { + return _channel(mapId).invokeMethod('map#takeSnapshot'); + } + + Widget _buildView( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required MapWidgetConfiguration widgetConfiguration, + MapObjects mapObjects = const MapObjects(), + Map mapOptions = const {}, + }) { + final Map creationParams = { + 'initialCameraPosition': + widgetConfiguration.initialCameraPosition.toMap(), + 'options': mapOptions, + 'markersToAdd': serializeMarkerSet(mapObjects.markers), + 'polygonsToAdd': serializePolygonSet(mapObjects.polygons), + 'polylinesToAdd': serializePolylineSet(mapObjects.polylines), + 'circlesToAdd': serializeCircleSet(mapObjects.circles), + 'tileOverlaysToAdd': serializeTileOverlaySet(mapObjects.tileOverlays), + }; + + return UiKitView( + viewType: 'plugins.flutter.dev/google_maps_ios', + onPlatformViewCreated: onPlatformViewCreated, + gestureRecognizers: widgetConfiguration.gestureRecognizers, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } + + @override + Widget buildViewWithConfiguration( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required MapWidgetConfiguration widgetConfiguration, + MapConfiguration mapConfiguration = const MapConfiguration(), + MapObjects mapObjects = const MapObjects(), + }) { + return _buildView( + creationId, + onPlatformViewCreated, + widgetConfiguration: widgetConfiguration, + mapObjects: mapObjects, + mapOptions: _jsonForMapConfiguration(mapConfiguration), + ); + } + + @override + Widget buildViewWithTextDirection( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + required TextDirection textDirection, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + return _buildView( + creationId, + onPlatformViewCreated, + widgetConfiguration: MapWidgetConfiguration( + initialCameraPosition: initialCameraPosition, + textDirection: textDirection), + mapObjects: MapObjects( + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + tileOverlays: tileOverlays), + mapOptions: mapOptions, + ); + } + + @override + Widget buildView( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + return buildViewWithTextDirection( + creationId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + textDirection: TextDirection.ltr, + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + tileOverlays: tileOverlays, + gestureRecognizers: gestureRecognizers, + mapOptions: mapOptions, + ); + } + + @override + @visibleForTesting + void enableDebugInspection() { + GoogleMapsInspectorPlatform.instance = + GoogleMapsInspectorIOS((int mapId) => _channel(mapId)); + } +} + +Map _jsonForMapConfiguration(MapConfiguration config) { + final EdgeInsets? padding = config.padding; + return { + if (config.compassEnabled != null) 'compassEnabled': config.compassEnabled!, + if (config.mapToolbarEnabled != null) + 'mapToolbarEnabled': config.mapToolbarEnabled!, + if (config.cameraTargetBounds != null) + 'cameraTargetBounds': config.cameraTargetBounds!.toJson(), + if (config.mapType != null) 'mapType': config.mapType!.index, + if (config.minMaxZoomPreference != null) + 'minMaxZoomPreference': config.minMaxZoomPreference!.toJson(), + if (config.rotateGesturesEnabled != null) + 'rotateGesturesEnabled': config.rotateGesturesEnabled!, + if (config.scrollGesturesEnabled != null) + 'scrollGesturesEnabled': config.scrollGesturesEnabled!, + if (config.tiltGesturesEnabled != null) + 'tiltGesturesEnabled': config.tiltGesturesEnabled!, + if (config.zoomControlsEnabled != null) + 'zoomControlsEnabled': config.zoomControlsEnabled!, + if (config.zoomGesturesEnabled != null) + 'zoomGesturesEnabled': config.zoomGesturesEnabled!, + if (config.liteModeEnabled != null) + 'liteModeEnabled': config.liteModeEnabled!, + if (config.trackCameraPosition != null) + 'trackCameraPosition': config.trackCameraPosition!, + if (config.myLocationEnabled != null) + 'myLocationEnabled': config.myLocationEnabled!, + if (config.myLocationButtonEnabled != null) + 'myLocationButtonEnabled': config.myLocationButtonEnabled!, + if (padding != null) + 'padding': [ + padding.top, + padding.left, + padding.bottom, + padding.right, + ], + if (config.indoorViewEnabled != null) + 'indoorEnabled': config.indoorViewEnabled!, + if (config.trafficEnabled != null) 'trafficEnabled': config.trafficEnabled!, + if (config.buildingsEnabled != null) + 'buildingsEnabled': config.buildingsEnabled!, + }; +} + +/// Update specification for a set of [TileOverlay]s. +// TODO(stuartmorgan): Fix the missing export of this class in the platform +// interface, and remove this copy. +class _TileOverlayUpdates extends MapsObjectUpdates { + /// Computes [TileOverlayUpdates] given previous and current [TileOverlay]s. + _TileOverlayUpdates.from(Set previous, Set current) + : super.from(previous, current, objectName: 'tileOverlay'); + + /// Set of TileOverlays to be added in this update. + Set get tileOverlaysToAdd => objectsToAdd; + + /// Set of TileOverlayIds to be removed in this update. + Set get tileOverlayIdsToRemove => + objectIdsToRemove.cast(); + + /// Set of TileOverlays to be changed in this update. + Set get tileOverlaysToChange => objectsToChange; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml new file mode 100644 index 000000000000..7ca13a9273f2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml @@ -0,0 +1,29 @@ +name: google_maps_flutter_ios +description: iOS implementation of the google_maps_flutter plugin. +repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter_ios +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 +version: 2.1.12 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: google_maps_flutter + platforms: + ios: + pluginClass: FLTGoogleMapsPlugin + dartPluginClass: GoogleMapsFlutterIOS + +dependencies: + flutter: + sdk: flutter + google_maps_flutter_platform_interface: ^2.2.1 + stream_transform: ^2.0.0 + +dev_dependencies: + async: ^2.5.0 + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 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 new file mode 100644 index 000000000000..136481cf3abb --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart @@ -0,0 +1,124 @@ +// 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:async/async.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_ios/google_maps_flutter_ios.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late List log; + + setUp(() async { + log = []; + }); + + /// Initializes a map with the given ID and canned responses, logging all + /// calls to [log]. + void configureMockMap( + GoogleMapsFlutterIOS maps, { + required int mapId, + required Future? Function(MethodCall call) handler, + }) { + maps + .ensureChannelInitialized(mapId) + .setMockMethodCallHandler((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 + .handlePlatformMessage('plugins.flutter.dev/google_maps_ios_$mapId', + byteData, (ByteData? data) {}); + } + + test('registers instance', () async { + GoogleMapsFlutterIOS.registerWith(); + expect(GoogleMapsFlutterPlatform.instance, isA()); + }); + + // Calls each method that uses invokeMethod with a return type other than + // void to ensure that the casting/nullability handling succeeds. + // + // TODO(stuartmorgan): Remove this once there is real test coverage of + // each method, since that would cover this issue. + test('non-void invokeMethods handle types correctly', () async { + const int mapId = 0; + final GoogleMapsFlutterIOS maps = GoogleMapsFlutterIOS(); + configureMockMap(maps, mapId: mapId, + handler: (MethodCall methodCall) async { + switch (methodCall.method) { + case 'map#getLatLng': + return [1.0, 2.0]; + case 'markers#isInfoWindowShown': + return true; + case 'map#getZoomLevel': + return 2.5; + case 'map#takeSnapshot': + return null; + } + }); + + await maps.getLatLng(const ScreenCoordinate(x: 0, y: 0), mapId: mapId); + await maps.isMarkerInfoWindowShown(const MarkerId(''), mapId: mapId); + await maps.getZoomLevel(mapId: mapId); + await maps.takeSnapshot(mapId: mapId); + // Check that all the invokeMethod calls happened. + expect(log, [ + 'map#getLatLng', + 'markers#isInfoWindowShown', + 'map#getZoomLevel', + 'map#takeSnapshot', + ]); + }); + + test('markers send drag event to correct streams', () async { + const int mapId = 1; + final Map jsonMarkerDragStartEvent = { + 'mapId': mapId, + 'markerId': 'drag-start-marker', + 'position': [1.0, 1.0] + }; + final Map jsonMarkerDragEvent = { + 'mapId': mapId, + 'markerId': 'drag-marker', + 'position': [1.0, 1.0] + }; + final Map jsonMarkerDragEndEvent = { + 'mapId': mapId, + 'markerId': 'drag-end-marker', + 'position': [1.0, 1.0] + }; + + final GoogleMapsFlutterIOS maps = GoogleMapsFlutterIOS(); + maps.ensureChannelInitialized(mapId); + + final StreamQueue markerDragStartStream = + StreamQueue(maps.onMarkerDragStart(mapId: mapId)); + final StreamQueue markerDragStream = + StreamQueue(maps.onMarkerDrag(mapId: mapId)); + final StreamQueue markerDragEndStream = + StreamQueue(maps.onMarkerDragEnd(mapId: mapId)); + + await sendPlatformMessage( + mapId, 'marker#onDragStart', jsonMarkerDragStartEvent); + await sendPlatformMessage(mapId, 'marker#onDrag', jsonMarkerDragEvent); + await sendPlatformMessage( + mapId, 'marker#onDragEnd', jsonMarkerDragEndEvent); + + expect((await markerDragStartStream.next).value.value, + equals('drag-start-marker')); + expect((await markerDragStream.next).value.value, equals('drag-marker')); + expect((await markerDragEndStream.next).value.value, + equals('drag-end-marker')); + }); +} 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 d1b4ef8c18cf..a41d1fe487f3 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,42 @@ +## 2.2.4 + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. + +## 2.2.3 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. + +## 2.2.2 + +* Adds a `size` parameter to `BitmapDescriptor.fromBytes`, so **web** applications + can specify the actual *physical size* of the bitmap. The parameter is not needed + (and ignored) in other platforms. Issue [#73789](https://github.com/flutter/flutter/issues/73789). +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 2.2.1 + +* Adds a new interface for inspecting the platform map state in tests. + +## 2.2.0 + +* Adds new versions of `buildView` and `updateOptions` that take a new option + class instead of a dictionary, to remove the cross-package dependency on + magic string keys. +* Adopts several parameter objects in the new `buildView` variant to + future-proof it against future changes. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/104231). + +## 2.1.7 + +* Updates code for stricter analysis options. +* Removes unnecessary imports. + +## 2.1.6 + +* Migrates from `ui.hash*` to `Object.hash*`. +* Updates minimum Flutter version to 2.5.0. + ## 2.1.5 Removes dependency on `meta`. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/analysis_options.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/analysis_options.yaml deleted file mode 100644 index 5aeb4e7c5e21..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../../analysis_options_legacy.yaml diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart index 300700071102..6484ae6b573d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart @@ -2,8 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'src/events/map_event.dart'; export 'src/method_channel/method_channel_google_maps_flutter.dart' show MethodChannelGoogleMapsFlutter; export 'src/platform_interface/google_maps_flutter_platform.dart'; +export 'src/platform_interface/google_maps_inspector_platform.dart'; export 'src/types/types.dart'; -export 'src/events/map_event.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart index 614cbe8e29fb..5961406c155c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart @@ -2,8 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; +import '../../google_maps_flutter_platform_interface.dart'; /// Generic Event coming from the native side of Maps. /// @@ -35,29 +34,29 @@ import 'package:google_maps_flutter_platform_interface/src/method_channel/method /// events to access the `.position` property, rather than the more generic `.value` /// yielded from the latter. class MapEvent { - /// The ID of the Map this event is associated to. - final int mapId; - - /// The value wrapped by this event - final T value; - /// Build a Map Event, that relates a mapId with a given value. /// /// The `mapId` is the id of the map that triggered the event. /// `value` may be `null` in events that don't transport any meaningful data. MapEvent(this.mapId, this.value); + + /// The ID of the Map this event is associated to. + final int mapId; + + /// The value wrapped by this event + final T value; } /// A `MapEvent` associated to a `position`. class _PositionedMapEvent extends MapEvent { - /// The position where this event happened. - final LatLng position; - /// Build a Positioned MapEvent, that relates a mapId and a position with a value. /// /// The `mapId` is the id of the map that triggered the event. /// `value` may be `null` in events that don't transport any meaningful data. _PositionedMapEvent(int mapId, this.position, T value) : super(mapId, value); + + /// The position where this event happened. + final LatLng position; } // The following events are the ones exposed to the end user. They are semantic extensions diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index 99f4fddaccd3..e17510f90624 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart @@ -3,6 +3,8 @@ // found in the LICENSE file. 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'; import 'package:flutter/foundation.dart'; @@ -10,11 +12,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:stream_transform/stream_transform.dart'; +import '../../google_maps_flutter_platform_interface.dart'; import '../types/tile_overlay_updates.dart'; -import '../types/utils/tile_overlay.dart'; +import '../types/utils/map_configuration_serialization.dart'; /// Error thrown when an unknown map ID is provided to a method channel API. class UnknownMapIDError extends Error { @@ -28,11 +30,12 @@ class UnknownMapIDError extends Error { /// Message describing the assertion error. final Object? message; + @override String toString() { if (message != null) { - return "Unknown map ID $mapId: ${Error.safeToString(message)}"; + return 'Unknown map ID $mapId: ${Error.safeToString(message)}'; } - return "Unknown map ID $mapId"; + return 'Unknown map ID $mapId'; } } @@ -49,11 +52,11 @@ class UnknownMapIDError extends Error { class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { // Keep a collection of id -> channel // Every method call passes the int mapId - final Map _channels = {}; + final Map _channels = {}; /// Accesses the MethodChannel associated to the passed mapId. MethodChannel channel(int mapId) { - MethodChannel? channel = _channels[mapId]; + final MethodChannel? channel = _channels[mapId]; if (channel == null) { throw UnknownMapIDError(mapId); } @@ -61,7 +64,8 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { } // Keep a collection of mapId to a map of TileOverlays. - final Map> _tileOverlays = {}; + final Map> _tileOverlays = + >{}; /// Returns the channel for [mapId], creating it if it doesn't already exist. @visibleForTesting @@ -78,7 +82,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { @override Future init(int mapId) { - MethodChannel channel = ensureChannelInitialized(mapId); + final MethodChannel channel = ensureChannelInitialized(mapId); return channel.invokeMethod('map#waitForMap'); } @@ -92,12 +96,13 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { // // It is a `broadcast` because multiple controllers will connect to // different stream views of this Controller. - final StreamController _mapEventStreamController = - StreamController.broadcast(); + final StreamController> _mapEventStreamController = + StreamController>.broadcast(); // Returns a filtered view of the events in the _controller, by mapId. - Stream _events(int mapId) => - _mapEventStreamController.stream.where((event) => event.mapId == mapId); + Stream> _events(int mapId) => + _mapEventStreamController.stream + .where((MapEvent event) => event.mapId == mapId); @override Stream onCameraMoveStarted({required int mapId}) { @@ -181,52 +186,52 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { case 'marker#onTap': _mapEventStreamController.add(MarkerTapEvent( mapId, - MarkerId(call.arguments['markerId']), + MarkerId(call.arguments['markerId'] as String), )); break; case 'marker#onDragStart': _mapEventStreamController.add(MarkerDragStartEvent( mapId, LatLng.fromJson(call.arguments['position'])!, - MarkerId(call.arguments['markerId']), + MarkerId(call.arguments['markerId'] as String), )); break; case 'marker#onDrag': _mapEventStreamController.add(MarkerDragEvent( mapId, LatLng.fromJson(call.arguments['position'])!, - MarkerId(call.arguments['markerId']), + MarkerId(call.arguments['markerId'] as String), )); break; case 'marker#onDragEnd': _mapEventStreamController.add(MarkerDragEndEvent( mapId, LatLng.fromJson(call.arguments['position'])!, - MarkerId(call.arguments['markerId']), + MarkerId(call.arguments['markerId'] as String), )); break; case 'infoWindow#onTap': _mapEventStreamController.add(InfoWindowTapEvent( mapId, - MarkerId(call.arguments['markerId']), + MarkerId(call.arguments['markerId'] as String), )); break; case 'polyline#onTap': _mapEventStreamController.add(PolylineTapEvent( mapId, - PolylineId(call.arguments['polylineId']), + PolylineId(call.arguments['polylineId'] as String), )); break; case 'polygon#onTap': _mapEventStreamController.add(PolygonTapEvent( mapId, - PolygonId(call.arguments['polygonId']), + PolygonId(call.arguments['polygonId'] as String), )); break; case 'circle#onTap': _mapEventStreamController.add(CircleTapEvent( mapId, - CircleId(call.arguments['circleId']), + CircleId(call.arguments['circleId'] as String), )); break; case 'map#onTap': @@ -244,17 +249,17 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { case 'tileOverlay#getTile': final Map? tileOverlaysForThisMap = _tileOverlays[mapId]; - final String tileOverlayId = call.arguments['tileOverlayId']; + final String tileOverlayId = call.arguments['tileOverlayId'] as String; final TileOverlay? tileOverlay = tileOverlaysForThisMap?[TileOverlayId(tileOverlayId)]; - TileProvider? tileProvider = tileOverlay?.tileProvider; + final TileProvider? tileProvider = tileOverlay?.tileProvider; if (tileProvider == null) { return TileProvider.noTile.toJson(); } final Tile tile = await tileProvider.getTile( - call.arguments['x'], - call.arguments['y'], - call.arguments['zoom'], + call.arguments['x'] as int, + call.arguments['y'] as int, + call.arguments['zoom'] as int?, ); return tile.toJson(); default: @@ -331,7 +336,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { }) { final Map? currentTileOverlays = _tileOverlays[mapId]; - Set previousSet = currentTileOverlays != null + final Set previousSet = currentTileOverlays != null ? currentTileOverlays.values.toSet() : {}; final TileOverlayUpdates updates = @@ -381,9 +386,9 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { }) async { final List successAndError = (await channel(mapId) .invokeMethod>('map#setStyle', mapStyle))!; - final bool success = successAndError[0]; + final bool success = successAndError[0] as bool; if (!success) { - throw MapStyleException(successAndError[1]); + throw MapStyleException(successAndError[1] as String); } } @@ -419,7 +424,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { final List latLng = (await channel(mapId) .invokeMethod>( 'map#getLatLng', screenCoordinate.toJson()))!; - return LatLng(latLng[0], latLng[1]); + return LatLng(latLng[0] as double, latLng[1] as double); } @override @@ -480,28 +485,22 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { /// Defaults to false. bool useAndroidViewSurface = false; - @override - Widget buildViewWithTextDirection( + Widget _buildView( int creationId, PlatformViewCreatedCallback onPlatformViewCreated, { - required CameraPosition initialCameraPosition, - required TextDirection textDirection, - Set markers = const {}, - Set polygons = const {}, - Set polylines = const {}, - Set circles = const {}, - Set tileOverlays = const {}, - Set>? gestureRecognizers, + required MapWidgetConfiguration widgetConfiguration, + MapObjects mapObjects = const MapObjects(), Map mapOptions = const {}, }) { final Map creationParams = { - 'initialCameraPosition': initialCameraPosition.toMap(), + 'initialCameraPosition': + widgetConfiguration.initialCameraPosition.toMap(), 'options': mapOptions, - 'markersToAdd': serializeMarkerSet(markers), - 'polygonsToAdd': serializePolygonSet(polygons), - 'polylinesToAdd': serializePolylineSet(polylines), - 'circlesToAdd': serializeCircleSet(circles), - 'tileOverlaysToAdd': serializeTileOverlaySet(tileOverlays), + 'markersToAdd': serializeMarkerSet(mapObjects.markers), + 'polygonsToAdd': serializePolygonSet(mapObjects.polygons), + 'polylinesToAdd': serializePolylineSet(mapObjects.polylines), + 'circlesToAdd': serializeCircleSet(mapObjects.circles), + 'tileOverlaysToAdd': serializeTileOverlaySet(mapObjects.tileOverlays), }; if (defaultTargetPlatform == TargetPlatform.android) { @@ -514,8 +513,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ) { return AndroidViewSurface( controller: controller as AndroidViewController, - gestureRecognizers: gestureRecognizers ?? - const >{}, + gestureRecognizers: widgetConfiguration.gestureRecognizers, hitTestBehavior: PlatformViewHitTestBehavior.opaque, ); }, @@ -524,7 +522,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { PlatformViewsService.initSurfaceAndroidView( id: params.id, viewType: 'plugins.flutter.io/google_maps', - layoutDirection: textDirection, + layoutDirection: widgetConfiguration.textDirection, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), onFocus: () => params.onFocusChanged(true), @@ -544,7 +542,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { return AndroidView( viewType: 'plugins.flutter.io/google_maps', onPlatformViewCreated: onPlatformViewCreated, - gestureRecognizers: gestureRecognizers, + gestureRecognizers: widgetConfiguration.gestureRecognizers, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), ); @@ -553,7 +551,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { return UiKitView( viewType: 'plugins.flutter.io/google_maps', onPlatformViewCreated: onPlatformViewCreated, - gestureRecognizers: gestureRecognizers, + gestureRecognizers: widgetConfiguration.gestureRecognizers, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), ); @@ -563,6 +561,53 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { '$defaultTargetPlatform is not yet supported by the maps plugin'); } + @override + Widget buildViewWithConfiguration( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required MapWidgetConfiguration widgetConfiguration, + MapConfiguration mapConfiguration = const MapConfiguration(), + MapObjects mapObjects = const MapObjects(), + }) { + return _buildView( + creationId, + onPlatformViewCreated, + widgetConfiguration: widgetConfiguration, + mapObjects: mapObjects, + mapOptions: jsonForMapConfiguration(mapConfiguration), + ); + } + + @override + Widget buildViewWithTextDirection( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + required TextDirection textDirection, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + return _buildView( + creationId, + onPlatformViewCreated, + widgetConfiguration: MapWidgetConfiguration( + initialCameraPosition: initialCameraPosition, + textDirection: textDirection), + mapObjects: MapObjects( + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + tileOverlays: tileOverlays), + mapOptions: mapOptions, + ); + } + @override Widget buildView( int creationId, diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart index 6b39973134be..147d64f715b7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart @@ -3,18 +3,20 @@ // found in the LICENSE file. 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'; -import 'package:flutter/widgets.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; - -import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; -import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import '../../google_maps_flutter_platform_interface.dart'; +import '../types/utils/map_configuration_serialization.dart'; + /// The interface that platform-specific implementations of `google_maps_flutter` must extend. /// /// Avoid `implements` of this interface. Using `implements` makes adding any new @@ -50,7 +52,8 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('init() has not been implemented.'); } - /// Updates configuration options of the map user interface. + /// Updates configuration options of the map user interface - deprecated, use + /// updateMapConfiguration instead. /// /// Change listeners are notified once the update has been made on the /// platform side. @@ -63,6 +66,20 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('updateMapOptions() has not been implemented.'); } + /// Updates configuration options of the map user interface. + /// + /// Change listeners are notified once the update has been made on the + /// platform side. + /// + /// The returned [Future] completes after listeners have been notified. + Future updateMapConfiguration( + MapConfiguration configuration, { + required int mapId, + }) { + return updateMapOptions(jsonForMapConfiguration(configuration), + mapId: mapId); + } + /// Updates marker configuration. /// /// Change listeners are notified once the update has been made on the @@ -348,7 +365,8 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('dispose() has not been implemented.'); } - /// Returns a widget displaying the map view. + /// Returns a widget displaying the map view - deprecated, use + /// [buildViewWithConfiguration] instead. Widget buildView( int creationId, PlatformViewCreatedCallback onPlatformViewCreated, { @@ -360,14 +378,15 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { Set tileOverlays = const {}, Set>? gestureRecognizers = const >{}, - // TODO: Replace with a structured type that's part of the interface. - // See https://github.com/flutter/flutter/issues/70330. + // TODO(stuartmorgan): Replace with a structured type that's part of the + // interface. See https://github.com/flutter/flutter/issues/70330. Map mapOptions = const {}, }) { throw UnimplementedError('buildView() has not been implemented.'); } - /// Returns a widget displaying the map view. + /// Returns a widget displaying the map view - deprecated, use + /// [buildViewWithConfiguration] instead. /// /// This method is similar to [buildView], but contains a parameter for /// platforms that require a text direction. @@ -381,12 +400,12 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { PlatformViewCreatedCallback onPlatformViewCreated, { required CameraPosition initialCameraPosition, required TextDirection textDirection, + Set>? gestureRecognizers, Set markers = const {}, Set polygons = const {}, Set polylines = const {}, Set circles = const {}, Set tileOverlays = const {}, - Set>? gestureRecognizers, Map mapOptions = const {}, }) { return buildView( @@ -402,4 +421,35 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { mapOptions: mapOptions, ); } + + /// Returns a widget displaying the map view. + Widget buildViewWithConfiguration( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required MapWidgetConfiguration widgetConfiguration, + MapConfiguration mapConfiguration = const MapConfiguration(), + MapObjects mapObjects = const MapObjects(), + }) { + return buildViewWithTextDirection( + creationId, + onPlatformViewCreated, + initialCameraPosition: widgetConfiguration.initialCameraPosition, + textDirection: widgetConfiguration.textDirection, + markers: mapObjects.markers, + polygons: mapObjects.polygons, + polylines: mapObjects.polylines, + circles: mapObjects.circles, + tileOverlays: mapObjects.tileOverlays, + gestureRecognizers: widgetConfiguration.gestureRecognizers, + mapOptions: jsonForMapConfiguration(mapConfiguration), + ); + } + + /// Populates [GoogleMapsFlutterInspectorPlatform.instance] to allow + /// inspecting the platform map state. + @visibleForTesting + void enableDebugInspection() { + throw UnimplementedError( + 'enableDebugInspection() has not been implemented.'); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart new file mode 100644 index 000000000000..1e07b97c300d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart @@ -0,0 +1,118 @@ +// 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:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../../google_maps_flutter_platform_interface.dart'; + +/// The interface that platform-specific implementations of +/// `google_maps_flutter` can extend to support state inpsection in tests. +/// +/// Avoid `implements` of this interface. Using `implements` makes adding any +/// new methods here a breaking change for end users of your platform! +/// +/// Do `extends GoogleMapsInspectorPlatform` instead, so new methods +/// added here are inherited in your code with the default implementation (that +/// throws at runtime), rather than breaking your users at compile time. +abstract class GoogleMapsInspectorPlatform extends PlatformInterface { + /// Constructs a GoogleMapsFlutterPlatform. + GoogleMapsInspectorPlatform() : super(token: _token); + + static final Object _token = Object(); + + static GoogleMapsInspectorPlatform? _instance; + + /// The instance of [GoogleMapsInspectorPlatform], if any. + /// + /// This is usually populated by calling + /// [GoogleMapsFlutterPlatform.enableDebugInspection]. + static GoogleMapsInspectorPlatform? get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [GoogleMapsInspectorPlatform] in their + /// implementation of [GoogleMapsFlutterPlatform.enableDebugInspection]. + static set instance(GoogleMapsInspectorPlatform? instance) { + if (instance != null) { + PlatformInterface.verify(instance, _token); + } + _instance = instance; + } + + /// Returns the minimum and maxmimum zoom level settings. + Future getMinMaxZoomLevels({required int mapId}) { + throw UnimplementedError('getMinMaxZoomLevels() has not been implemented.'); + } + + /// Returns true if the compass is enabled. + Future isCompassEnabled({required int mapId}) { + throw UnimplementedError('isCompassEnabled() has not been implemented.'); + } + + /// Returns true if lite mode is enabled. + Future isLiteModeEnabled({required int mapId}) { + throw UnimplementedError('isLiteModeEnabled() has not been implemented.'); + } + + /// Returns true if the map toolbar is enabled. + Future isMapToolbarEnabled({required int mapId}) { + throw UnimplementedError('isMapToolbarEnabled() has not been implemented.'); + } + + /// Returns true if the "my location" button is enabled. + Future isMyLocationButtonEnabled({required int mapId}) { + throw UnimplementedError( + 'isMyLocationButtonEnabled() has not been implemented.'); + } + + /// Returns true if the traffic overlay is enabled. + Future isTrafficEnabled({required int mapId}) { + throw UnimplementedError('isTrafficEnabled() has not been implemented.'); + } + + /// Returns true if the building layer is enabled. + Future areBuildingsEnabled({required int mapId}) { + throw UnimplementedError('areBuildingsEnabled() has not been implemented.'); + } + + /// Returns true if rotate gestures are enabled. + Future areRotateGesturesEnabled({required int mapId}) { + throw UnimplementedError( + 'areRotateGesturesEnabled() has not been implemented.'); + } + + /// Returns true if scroll gestures are enabled. + Future areScrollGesturesEnabled({required int mapId}) { + throw UnimplementedError( + 'areScrollGesturesEnabled() has not been implemented.'); + } + + /// Returns true if tilt gestures are enabled. + Future areTiltGesturesEnabled({required int mapId}) { + throw UnimplementedError( + 'areTiltGesturesEnabled() has not been implemented.'); + } + + /// Returns true if zoom controls are enabled. + Future areZoomControlsEnabled({required int mapId}) { + throw UnimplementedError( + 'areZoomControlsEnabled() has not been implemented.'); + } + + /// Returns true if zoom gestures are enabled. + Future areZoomGesturesEnabled({required int mapId}) { + throw UnimplementedError( + 'areZoomGesturesEnabled() has not been implemented.'); + } + + /// Returns information about the tile overlay with the given ID. + /// + /// The returned object will be synthesized from platform data, so will not + /// be the same Dart object as the original [TileOverlay] provided to the + /// platform interface with that ID, and not all fields (e.g., + /// [TileOverlay.tileProvider]) will be populated. + Future getTileOverlayInfo(TileOverlayId tileOverlayId, + {required int mapId}) { + throw UnimplementedError('getTileOverlayInfo() has not been implemented.'); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart index d3dc37e327fe..7dda43a7abf4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart @@ -6,24 +6,68 @@ import 'dart:async' show Future; import 'dart:typed_data' show Uint8List; import 'dart:ui' show Size; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart' show ImageConfiguration, AssetImage, AssetBundleImageKey; import 'package:flutter/services.dart' show AssetBundle; -import 'package:flutter/foundation.dart' show kIsWeb; - /// Defines a bitmap image. For a marker, this class can be used to set the /// image of the marker icon. For a ground overlay, it can be used to set the /// image to place on the surface of the earth. class BitmapDescriptor { const BitmapDescriptor._(this._json); + /// The inverse of .toJson. + // TODO(stuartmorgan): Remove this in the next breaking change. + @Deprecated('No longer supported') + BitmapDescriptor.fromJson(Object json) : _json = json { + assert(_json is List); + final List jsonList = json as List; + assert(_validTypes.contains(jsonList[0])); + switch (jsonList[0]) { + case _defaultMarker: + assert(jsonList.length <= 2); + if (jsonList.length == 2) { + assert(jsonList[1] is num); + final num secondElement = jsonList[1] as num; + assert(0 <= secondElement && secondElement < 360); + } + break; + case _fromBytes: + assert(jsonList.length == 2); + assert(jsonList[1] != null && jsonList[1] is List); + assert((jsonList[1] as List).isNotEmpty); + break; + case _fromAsset: + assert(jsonList.length <= 3); + assert(jsonList[1] != null && jsonList[1] is String); + assert((jsonList[1] as String).isNotEmpty); + if (jsonList.length == 3) { + assert(jsonList[2] != null && jsonList[2] is String); + assert((jsonList[2] as String).isNotEmpty); + } + break; + case _fromAssetImage: + assert(jsonList.length <= 4); + assert(jsonList[1] != null && jsonList[1] is String); + assert((jsonList[1] as String).isNotEmpty); + assert(jsonList[2] != null && jsonList[2] is double); + if (jsonList.length == 4) { + assert(jsonList[3] != null && jsonList[3] is List); + assert((jsonList[3] as List).length == 2); + } + break; + default: + break; + } + } + static const String _defaultMarker = 'defaultMarker'; static const String _fromAsset = 'fromAsset'; static const String _fromAssetImage = 'fromAssetImage'; static const String _fromBytes = 'fromBytes'; - static const Set _validTypes = { + static const Set _validTypes = { _defaultMarker, _fromAsset, _fromAssetImage, @@ -86,7 +130,7 @@ class BitmapDescriptor { String? package, bool mipmaps = true, }) async { - double? devicePixelRatio = configuration.devicePixelRatio; + final double? devicePixelRatio = configuration.devicePixelRatio; if (!mipmaps && devicePixelRatio != null) { return BitmapDescriptor._([ _fromAssetImage, @@ -104,7 +148,7 @@ class BitmapDescriptor { assetBundleImageKey.name, assetBundleImageKey.scale, if (kIsWeb && size != null) - [ + [ size.width, size.height, ], @@ -113,53 +157,22 @@ class BitmapDescriptor { /// Creates a BitmapDescriptor using an array of bytes that must be encoded /// as PNG. - static BitmapDescriptor fromBytes(Uint8List byteData) { - return BitmapDescriptor._([_fromBytes, byteData]); - } - - /// The inverse of .toJson. - // This is needed in Web to re-hydrate BitmapDescriptors that have been - // transformed to JSON for transport. - // TODO(https://github.com/flutter/flutter/issues/70330): Clean this up. - BitmapDescriptor.fromJson(Object json) : _json = json { - assert(_json is List); - final jsonList = json as List; - assert(_validTypes.contains(jsonList[0])); - switch (jsonList[0]) { - case _defaultMarker: - assert(jsonList.length <= 2); - if (jsonList.length == 2) { - assert(jsonList[1] is num); - assert(0 <= jsonList[1] && jsonList[1] < 360); - } - break; - case _fromBytes: - assert(jsonList.length == 2); - assert(jsonList[1] != null && jsonList[1] is List); - assert((jsonList[1] as List).isNotEmpty); - break; - case _fromAsset: - assert(jsonList.length <= 3); - assert(jsonList[1] != null && jsonList[1] is String); - assert((jsonList[1] as String).isNotEmpty); - if (jsonList.length == 3) { - assert(jsonList[2] != null && jsonList[2] is String); - assert((jsonList[2] as String).isNotEmpty); - } - break; - case _fromAssetImage: - assert(jsonList.length <= 4); - assert(jsonList[1] != null && jsonList[1] is String); - assert((jsonList[1] as String).isNotEmpty); - assert(jsonList[2] != null && jsonList[2] is double); - if (jsonList.length == 4) { - assert(jsonList[3] != null && jsonList[3] is List); - assert((jsonList[3] as List).length == 2); - } - break; - default: - break; - } + /// On the web, the [size] parameter represents the *physical size* of the + /// bitmap, regardless of the actual resolution of the encoded PNG. + /// This helps the browser to render High-DPI images at the correct size. + /// `size` is not required (and ignored, if passed) in other platforms. + static BitmapDescriptor fromBytes(Uint8List byteData, {Size? size}) { + assert(byteData.isNotEmpty, + 'Cannot create BitmapDescriptor with empty byteData'); + return BitmapDescriptor._([ + _fromBytes, + byteData, + if (kIsWeb && size != null) + [ + size.width, + size.height, + ] + ]); } final Object _json; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/callbacks.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/callbacks.dart index 3b484c1feb05..5d6af90290e0 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/callbacks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/callbacks.dart @@ -10,10 +10,10 @@ import 'types.dart'; /// registers a camera movement. /// /// This is used in [GoogleMap.onCameraMove]. -typedef void CameraPositionCallback(CameraPosition position); +typedef CameraPositionCallback = void Function(CameraPosition position); /// Callback function taking a single argument. -typedef void ArgumentCallback(T argument); +typedef ArgumentCallback = void Function(T argument); /// Mutable collection of [ArgumentCallback] instances, itself an [ArgumentCallback]. /// @@ -35,7 +35,7 @@ class ArgumentCallbacks { if (length == 1) { _callbacks[0].call(argument); } else if (0 < length) { - for (ArgumentCallback callback + for (final ArgumentCallback callback in List>.from(_callbacks)) { callback(argument); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart index 7cb6369e7f59..6d1ce164238b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart @@ -2,7 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues, Offset; +import 'dart:ui' show Offset; + +import 'package:flutter/foundation.dart'; import 'types.dart'; @@ -10,6 +12,7 @@ import 'types.dart'; /// /// Aggregates the camera's [target] geographical location, its [zoom] level, /// [tilt] angle, and [bearing]. +@immutable class CameraPosition { /// Creates a immutable representation of the [GoogleMap] camera. /// @@ -72,7 +75,7 @@ class CameraPosition { /// /// Mainly for internal use. static CameraPosition? fromMap(Object? json) { - if (json == null || !(json is Map)) { + if (json == null || json is! Map) { return null; } final LatLng? target = LatLng.fromJson(json['target']); @@ -80,26 +83,30 @@ class CameraPosition { return null; } return CameraPosition( - bearing: json['bearing'], + bearing: json['bearing'] as double, target: target, - tilt: json['tilt'], - zoom: json['zoom'], + tilt: json['tilt'] as double, + zoom: json['zoom'] as double, ); } @override bool operator ==(Object other) { - if (identical(this, other)) return true; - if (runtimeType != other.runtimeType) return false; - final CameraPosition typedOther = other as CameraPosition; - return bearing == typedOther.bearing && - target == typedOther.target && - tilt == typedOther.tilt && - zoom == typedOther.zoom; + if (identical(this, other)) { + return true; + } + if (runtimeType != other.runtimeType) { + return false; + } + return other is CameraPosition && + bearing == other.bearing && + target == other.target && + tilt == other.tilt && + zoom == other.zoom; } @override - int get hashCode => hashValues(bearing, target, tilt, zoom); + int get hashCode => Object.hash(bearing, target, tilt, zoom); @override String toString() => diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart index 1e9b2181778e..d9e4b2d705c9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart @@ -3,8 +3,8 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart' show VoidCallback; -import 'package:flutter/material.dart' show Color, Colors; import 'package:flutter/foundation.dart' show immutable; +import 'package:flutter/material.dart' show Color, Colors; import 'types.dart'; @@ -105,9 +105,11 @@ class Circle implements MapsObject { } /// Creates a new [Circle] object whose values are the same as this instance. + @override Circle clone() => copyWith(); /// Converts this object to something serializable in JSON. + @override Object toJson() { final Map json = {}; @@ -132,18 +134,22 @@ class Circle implements MapsObject { @override bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final Circle typedOther = other as Circle; - return circleId == typedOther.circleId && - consumeTapEvents == typedOther.consumeTapEvents && - fillColor == typedOther.fillColor && - center == typedOther.center && - radius == typedOther.radius && - strokeColor == typedOther.strokeColor && - strokeWidth == typedOther.strokeWidth && - visible == typedOther.visible && - zIndex == typedOther.zIndex; + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is Circle && + circleId == other.circleId && + consumeTapEvents == other.consumeTapEvents && + fillColor == other.fillColor && + center == other.center && + radius == other.radius && + strokeColor == other.strokeColor && + strokeWidth == other.strokeWidth && + visible == other.visible && + zIndex == other.zIndex; } @override diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart index e2ca635be75e..81fe08bb1329 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart @@ -2,11 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - -import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter/foundation.dart' + show immutable, objectRuntimeType, visibleForTesting; /// A pair of latitude and longitude coordinates, stored as degrees. +@immutable class LatLng { /// Creates a geographical location specified in degrees [latitude] and /// [longitude]. @@ -19,7 +19,7 @@ class LatLng { : assert(latitude != null), assert(longitude != null), latitude = - (latitude < -90.0 ? -90.0 : (90.0 < latitude ? 90.0 : latitude)), + latitude < -90.0 ? -90.0 : (90.0 < latitude ? 90.0 : latitude), // Avoids normalization if possible to prevent unnecessary loss of precision longitude = longitude >= -180 && longitude < 180 ? longitude @@ -42,20 +42,23 @@ class LatLng { return null; } assert(json is List && json.length == 2); - final list = json as List; - return LatLng(list[0], list[1]); + final List list = json as List; + return LatLng(list[0]! as double, list[1]! as double); } @override - String toString() => '$runtimeType($latitude, $longitude)'; + String toString() => + '${objectRuntimeType(this, 'LatLng')}($latitude, $longitude)'; @override - bool operator ==(Object o) { - return o is LatLng && o.latitude == latitude && o.longitude == longitude; + bool operator ==(Object other) { + return other is LatLng && + other.latitude == latitude && + other.longitude == longitude; } @override - int get hashCode => hashValues(latitude, longitude); + int get hashCode => Object.hash(latitude, longitude); } /// A latitude/longitude aligned rectangle. @@ -66,6 +69,7 @@ class LatLng { /// if `southwest.longitude` ≤ `northeast.longitude`, /// * lng ∈ [-180, `northeast.longitude`] ∪ [`southwest.longitude`, 180], /// if `northeast.longitude` < `southwest.longitude` +@immutable class LatLngBounds { /// Creates geographical bounding box with the specified corners. /// @@ -112,7 +116,7 @@ class LatLngBounds { return null; } assert(json is List && json.length == 2); - final list = json as List; + final List list = json as List; return LatLngBounds( southwest: LatLng.fromJson(list[0])!, northeast: LatLng.fromJson(list[1])!, @@ -121,16 +125,16 @@ class LatLngBounds { @override String toString() { - return '$runtimeType($southwest, $northeast)'; + return '${objectRuntimeType(this, 'LatLngBounds')}($southwest, $northeast)'; } @override - bool operator ==(Object o) { - return o is LatLngBounds && - o.southwest == southwest && - o.northeast == northeast; + bool operator ==(Object other) { + return other is LatLngBounds && + other.southwest == southwest && + other.northeast == northeast; } @override - int get hashCode => hashValues(southwest, northeast); + int get hashCode => Object.hash(southwest, northeast); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_configuration.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_configuration.dart new file mode 100644 index 000000000000..4b43caffe5b6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_configuration.dart @@ -0,0 +1,248 @@ +// 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/widgets.dart'; + +import 'ui.dart'; + +/// Configuration options for the GoogleMaps user interface. +@immutable +class MapConfiguration { + /// Creates a new configuration instance with the given options. + /// + /// Any options that aren't passed will be null, which allows this to serve + /// as either a full configuration selection, or an update to an existing + /// configuration where only non-null values are updated. + const MapConfiguration({ + this.compassEnabled, + this.mapToolbarEnabled, + this.cameraTargetBounds, + this.mapType, + this.minMaxZoomPreference, + this.rotateGesturesEnabled, + this.scrollGesturesEnabled, + this.tiltGesturesEnabled, + this.trackCameraPosition, + this.zoomControlsEnabled, + this.zoomGesturesEnabled, + this.liteModeEnabled, + this.myLocationEnabled, + this.myLocationButtonEnabled, + this.padding, + this.indoorViewEnabled, + this.trafficEnabled, + this.buildingsEnabled, + }); + + /// True if the compass UI should be shown. + final bool? compassEnabled; + + /// True if the map toolbar should be shown. + final bool? mapToolbarEnabled; + + /// The bounds to display. + final CameraTargetBounds? cameraTargetBounds; + + /// The type of the map. + final MapType? mapType; + + /// The prefered zoom range. + final MinMaxZoomPreference? minMaxZoomPreference; + + /// True if rotate gestures should be enabled. + final bool? rotateGesturesEnabled; + + /// True if scroll gestures should be enabled. + final bool? scrollGesturesEnabled; + + /// True if tilt gestures should be enabled. + final bool? tiltGesturesEnabled; + + /// True if camera position changes should trigger notifications. + final bool? trackCameraPosition; + + /// True if zoom controls should be displayed. + final bool? zoomControlsEnabled; + + /// True if zoom gestures should be enabled. + final bool? zoomGesturesEnabled; + + /// True if the map should use Lite Mode, showing a limited-interactivity + /// bitmap, on supported platforms. + final bool? liteModeEnabled; + + /// True if the current location should be tracked and displayed. + final bool? myLocationEnabled; + + /// True if the control to jump to the current location should be displayed. + final bool? myLocationButtonEnabled; + + /// The padding for the map display. + final EdgeInsets? padding; + + /// True if indoor map views should be enabled. + final bool? indoorViewEnabled; + + /// True if the traffic overlay should be enabled. + final bool? trafficEnabled; + + /// True if 3D building display should be enabled. + final bool? buildingsEnabled; + + /// Returns a new options object containing only the values of this instance + /// that are different from [other]. + MapConfiguration diffFrom(MapConfiguration other) { + return MapConfiguration( + compassEnabled: + compassEnabled != other.compassEnabled ? compassEnabled : null, + mapToolbarEnabled: mapToolbarEnabled != other.mapToolbarEnabled + ? mapToolbarEnabled + : null, + cameraTargetBounds: cameraTargetBounds != other.cameraTargetBounds + ? cameraTargetBounds + : null, + mapType: mapType != other.mapType ? mapType : null, + minMaxZoomPreference: minMaxZoomPreference != other.minMaxZoomPreference + ? minMaxZoomPreference + : null, + rotateGesturesEnabled: + rotateGesturesEnabled != other.rotateGesturesEnabled + ? rotateGesturesEnabled + : null, + scrollGesturesEnabled: + scrollGesturesEnabled != other.scrollGesturesEnabled + ? scrollGesturesEnabled + : null, + tiltGesturesEnabled: tiltGesturesEnabled != other.tiltGesturesEnabled + ? tiltGesturesEnabled + : null, + trackCameraPosition: trackCameraPosition != other.trackCameraPosition + ? trackCameraPosition + : null, + zoomControlsEnabled: zoomControlsEnabled != other.zoomControlsEnabled + ? zoomControlsEnabled + : null, + zoomGesturesEnabled: zoomGesturesEnabled != other.zoomGesturesEnabled + ? zoomGesturesEnabled + : null, + liteModeEnabled: + liteModeEnabled != other.liteModeEnabled ? liteModeEnabled : null, + myLocationEnabled: myLocationEnabled != other.myLocationEnabled + ? myLocationEnabled + : null, + myLocationButtonEnabled: + myLocationButtonEnabled != other.myLocationButtonEnabled + ? myLocationButtonEnabled + : null, + padding: padding != other.padding ? padding : null, + indoorViewEnabled: indoorViewEnabled != other.indoorViewEnabled + ? indoorViewEnabled + : null, + trafficEnabled: + trafficEnabled != other.trafficEnabled ? trafficEnabled : null, + buildingsEnabled: + buildingsEnabled != other.buildingsEnabled ? buildingsEnabled : null, + ); + } + + /// Returns a copy of this instance with any non-null settings form [diff] + /// replacing the previous values. + MapConfiguration applyDiff(MapConfiguration diff) { + return MapConfiguration( + compassEnabled: diff.compassEnabled ?? compassEnabled, + mapToolbarEnabled: diff.mapToolbarEnabled ?? mapToolbarEnabled, + cameraTargetBounds: diff.cameraTargetBounds ?? cameraTargetBounds, + mapType: diff.mapType ?? mapType, + minMaxZoomPreference: diff.minMaxZoomPreference ?? minMaxZoomPreference, + rotateGesturesEnabled: + diff.rotateGesturesEnabled ?? rotateGesturesEnabled, + scrollGesturesEnabled: + diff.scrollGesturesEnabled ?? scrollGesturesEnabled, + tiltGesturesEnabled: diff.tiltGesturesEnabled ?? tiltGesturesEnabled, + trackCameraPosition: diff.trackCameraPosition ?? trackCameraPosition, + zoomControlsEnabled: diff.zoomControlsEnabled ?? zoomControlsEnabled, + zoomGesturesEnabled: diff.zoomGesturesEnabled ?? zoomGesturesEnabled, + liteModeEnabled: diff.liteModeEnabled ?? liteModeEnabled, + myLocationEnabled: diff.myLocationEnabled ?? myLocationEnabled, + myLocationButtonEnabled: + diff.myLocationButtonEnabled ?? myLocationButtonEnabled, + padding: diff.padding ?? padding, + indoorViewEnabled: diff.indoorViewEnabled ?? indoorViewEnabled, + trafficEnabled: diff.trafficEnabled ?? trafficEnabled, + buildingsEnabled: diff.buildingsEnabled ?? buildingsEnabled, + ); + } + + /// True if no options are set. + bool get isEmpty => + compassEnabled == null && + mapToolbarEnabled == null && + cameraTargetBounds == null && + mapType == null && + minMaxZoomPreference == null && + rotateGesturesEnabled == null && + scrollGesturesEnabled == null && + tiltGesturesEnabled == null && + trackCameraPosition == null && + zoomControlsEnabled == null && + zoomGesturesEnabled == null && + liteModeEnabled == null && + myLocationEnabled == null && + myLocationButtonEnabled == null && + padding == null && + indoorViewEnabled == null && + trafficEnabled == null && + buildingsEnabled == null; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is MapConfiguration && + compassEnabled == other.compassEnabled && + mapToolbarEnabled == other.mapToolbarEnabled && + cameraTargetBounds == other.cameraTargetBounds && + mapType == other.mapType && + minMaxZoomPreference == other.minMaxZoomPreference && + rotateGesturesEnabled == other.rotateGesturesEnabled && + scrollGesturesEnabled == other.scrollGesturesEnabled && + tiltGesturesEnabled == other.tiltGesturesEnabled && + trackCameraPosition == other.trackCameraPosition && + zoomControlsEnabled == other.zoomControlsEnabled && + zoomGesturesEnabled == other.zoomGesturesEnabled && + liteModeEnabled == other.liteModeEnabled && + myLocationEnabled == other.myLocationEnabled && + myLocationButtonEnabled == other.myLocationButtonEnabled && + padding == other.padding && + indoorViewEnabled == other.indoorViewEnabled && + trafficEnabled == other.trafficEnabled && + buildingsEnabled == other.buildingsEnabled; + } + + @override + int get hashCode => Object.hash( + compassEnabled, + mapToolbarEnabled, + cameraTargetBounds, + mapType, + minMaxZoomPreference, + rotateGesturesEnabled, + scrollGesturesEnabled, + tiltGesturesEnabled, + trackCameraPosition, + zoomControlsEnabled, + zoomGesturesEnabled, + liteModeEnabled, + myLocationEnabled, + myLocationButtonEnabled, + padding, + indoorViewEnabled, + trafficEnabled, + buildingsEnabled, + ); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_objects.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_objects.dart new file mode 100644 index 000000000000..56f80e8312dd --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_objects.dart @@ -0,0 +1,31 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/foundation.dart'; + +import 'types.dart'; + +/// A container object for all the types of maps objects. +/// +/// This is intended for use as a parameter in platform interface methods, to +/// allow adding new object types to existing methods. +@immutable +class MapObjects { + /// Creates a new set of map objects with all the given object types. + const MapObjects({ + this.markers = const {}, + this.polygons = const {}, + this.polylines = const {}, + this.circles = const {}, + this.tileOverlays = const {}, + }); + + final Set markers; + final Set polygons; + final Set polylines; + final Set circles; + final Set tileOverlays; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_widget_configuration.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_widget_configuration.dart new file mode 100644 index 000000000000..029af9901661 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_widget_configuration.dart @@ -0,0 +1,32 @@ +// 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/gestures.dart'; +import 'package:flutter/material.dart'; + +import 'types.dart'; + +/// A container object for configuration options when building a widget. +/// +/// This is intended for use as a parameter in platform interface methods, to +/// allow adding new configuration options to existing methods. +@immutable +class MapWidgetConfiguration { + /// Creates a new configuration with all the given settings. + const MapWidgetConfiguration({ + required this.initialCameraPosition, + required this.textDirection, + this.gestureRecognizers = const >{}, + }); + + /// The initial camera position to display. + final CameraPosition initialCameraPosition; + + /// The text direction for the widget. + final TextDirection textDirection; + + /// Gesture recognizers to add to the widget. + final Set> gestureRecognizers; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart index be629e174143..953746daa745 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart @@ -20,10 +20,13 @@ class MapsObjectId { @override bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final MapsObjectId typedOther = other as MapsObjectId; - return value == typedOther.value; + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is MapsObjectId && value == other.value; } @override diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart index 2e2eefa3d32e..efc319b60ced 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart @@ -2,15 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues, hashList; - -import 'package:flutter/foundation.dart' show objectRuntimeType, setEquals; +import 'package:flutter/foundation.dart' + show immutable, objectRuntimeType, setEquals; import 'maps_object.dart'; import 'utils/maps_object.dart'; /// Update specification for a set of objects. -class MapsObjectUpdates { +@immutable +class MapsObjectUpdates> { /// Computes updates given previous and current object sets. /// /// [objectName] is the prefix to use when serializing the updates into a JSON @@ -31,7 +31,7 @@ class MapsObjectUpdates { /// /// It is a programming error to call this with an ID that is not guaranteed /// to be in [currentObjects]. - T _idToCurrentObject(MapsObjectId id) { + T idToCurrentObject(MapsObjectId id) { return currentObjects[id]!; } @@ -39,19 +39,19 @@ class MapsObjectUpdates { _objectsToAdd = currentObjectIds .difference(previousObjectIds) - .map(_idToCurrentObject) + .map(idToCurrentObject) .toSet(); // Returns `true` if [current] is not equals to previous one with the // same id. bool hasChanged(T current) { - final T? previous = previousObjects[current.mapsId as MapsObjectId]; + final T? previous = previousObjects[current.mapsId]; return current != previous; } _objectsToChange = currentObjectIds .intersection(previousObjectIds) - .map(_idToCurrentObject) + .map(idToCurrentObject) .where(hasChanged) .toSet(); } @@ -64,21 +64,21 @@ class MapsObjectUpdates { return _objectsToAdd; } - late Set _objectsToAdd; + late final Set _objectsToAdd; /// Set of objects to be removed in this update. Set> get objectIdsToRemove { return _objectIdsToRemove; } - late Set> _objectIdsToRemove; + late final Set> _objectIdsToRemove; /// Set of objects to be changed in this update. Set get objectsToChange { return _objectsToChange; } - late Set _objectsToChange; + late final Set _objectsToChange; /// Converts this object to JSON. Object toJson() { @@ -114,8 +114,8 @@ class MapsObjectUpdates { } @override - int get hashCode => hashValues(hashList(_objectsToAdd), - hashList(_objectIdsToRemove), hashList(_objectsToChange)); + int get hashCode => Object.hash(Object.hashAll(_objectsToAdd), + Object.hashAll(_objectIdsToRemove), Object.hashAll(_objectsToChange)); @override String toString() { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart index 5e62f7d9ffba..914e77a64c9f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues, Offset; +import 'dart:ui' show Offset; import 'package:flutter/foundation.dart' show immutable, ValueChanged, VoidCallback; @@ -14,6 +14,7 @@ Object _offsetToJson(Offset offset) { } /// Text labels for a [Marker] info window. +@immutable class InfoWindow { /// Creates an immutable representation of a label on for [Marker]. const InfoWindow({ @@ -81,16 +82,20 @@ class InfoWindow { @override bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final InfoWindow typedOther = other as InfoWindow; - return title == typedOther.title && - snippet == typedOther.snippet && - anchor == typedOther.anchor; + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is InfoWindow && + title == other.title && + snippet == other.snippet && + anchor == other.anchor; } @override - int get hashCode => hashValues(title.hashCode, snippet, anchor); + int get hashCode => Object.hash(title.hashCode, snippet, anchor); @override String toString() { @@ -113,7 +118,7 @@ class MarkerId extends MapsObjectId { /// the map's surface; that is, it will not necessarily change orientation /// due to map rotations, tilting, or zooming. @immutable -class Marker implements MapsObject { +class Marker implements MapsObject { /// Creates a set of marker configuration options. /// /// Default marker options. @@ -258,9 +263,11 @@ class Marker implements MapsObject { } /// Creates a new [Marker] object whose values are the same as this instance. + @override Marker clone() => copyWith(); /// Converts this object to something serializable in JSON. + @override Object toJson() { final Map json = {}; @@ -287,21 +294,25 @@ class Marker implements MapsObject { @override bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final Marker typedOther = other as Marker; - return markerId == typedOther.markerId && - alpha == typedOther.alpha && - anchor == typedOther.anchor && - consumeTapEvents == typedOther.consumeTapEvents && - draggable == typedOther.draggable && - flat == typedOther.flat && - icon == typedOther.icon && - infoWindow == typedOther.infoWindow && - position == typedOther.position && - rotation == typedOther.rotation && - visible == typedOther.visible && - zIndex == typedOther.zIndex; + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is Marker && + markerId == other.markerId && + alpha == other.alpha && + anchor == other.anchor && + consumeTapEvents == other.consumeTapEvents && + draggable == other.draggable && + flat == other.flat && + icon == other.icon && + infoWindow == other.infoWindow && + position == other.position && + rotation == other.rotation && + visible == other.visible && + zIndex == other.zIndex; } @override diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart index 7b6f24831e59..8653ba0ed0f6 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart @@ -20,7 +20,7 @@ class PolygonId extends MapsObjectId { /// Draws a polygon through geographical locations on the map. @immutable -class Polygon implements MapsObject { +class Polygon implements MapsObject { /// Creates an immutable representation of a polygon through geographical locations on the map. const Polygon({ required this.polygonId, @@ -123,11 +123,13 @@ class Polygon implements MapsObject { } /// Creates a new [Polygon] object whose values are the same as this instance. + @override Polygon clone() { return copyWith(pointsParam: List.of(points)); } /// Converts this object to something serializable in JSON. + @override Object toJson() { final Map json = {}; @@ -159,19 +161,23 @@ class Polygon implements MapsObject { @override bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final Polygon typedOther = other as Polygon; - return polygonId == typedOther.polygonId && - consumeTapEvents == typedOther.consumeTapEvents && - fillColor == typedOther.fillColor && - geodesic == typedOther.geodesic && - listEquals(points, typedOther.points) && - const DeepCollectionEquality().equals(holes, typedOther.holes) && - visible == typedOther.visible && - strokeColor == typedOther.strokeColor && - strokeWidth == typedOther.strokeWidth && - zIndex == typedOther.zIndex; + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is Polygon && + polygonId == other.polygonId && + consumeTapEvents == other.consumeTapEvents && + fillColor == other.fillColor && + geodesic == other.geodesic && + listEquals(points, other.points) && + const DeepCollectionEquality().equals(holes, other.holes) && + visible == other.visible && + strokeColor == other.strokeColor && + strokeWidth == other.strokeWidth && + zIndex == other.zIndex; } @override diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart index 00c718646229..39e62e3c0160 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart @@ -21,7 +21,7 @@ class PolylineId extends MapsObjectId { /// Draws a line through geographical locations on the map. @immutable -class Polyline implements MapsObject { +class Polyline implements MapsObject { /// Creates an immutable object representing a line drawn through geographical locations on the map. const Polyline({ required this.polylineId, @@ -150,6 +150,7 @@ class Polyline implements MapsObject { /// Creates a new [Polyline] object whose values are the same as this /// instance. + @override Polyline clone() { return copyWith( patternsParam: List.of(patterns), @@ -158,6 +159,7 @@ class Polyline implements MapsObject { } /// Converts this object to something serializable in JSON. + @override Object toJson() { final Map json = {}; @@ -191,21 +193,25 @@ class Polyline implements MapsObject { @override bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final Polyline typedOther = other as Polyline; - return polylineId == typedOther.polylineId && - consumeTapEvents == typedOther.consumeTapEvents && - color == typedOther.color && - geodesic == typedOther.geodesic && - jointType == typedOther.jointType && - listEquals(patterns, typedOther.patterns) && - listEquals(points, typedOther.points) && - startCap == typedOther.startCap && - endCap == typedOther.endCap && - visible == typedOther.visible && - width == typedOther.width && - zIndex == typedOther.zIndex; + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is Polyline && + polylineId == other.polylineId && + consumeTapEvents == other.consumeTapEvents && + color == other.color && + geodesic == other.geodesic && + jointType == other.jointType && + listEquals(patterns, other.patterns) && + listEquals(points, other.points) && + startCap == other.startCap && + endCap == other.endCap && + visible == other.visible && + width == other.width && + zIndex == other.zIndex; } @override diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart index 46bc6c12e509..b1d37dc2c234 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart @@ -2,9 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - -import 'package:flutter/foundation.dart' show immutable; +import 'package:flutter/foundation.dart' show immutable, objectRuntimeType; /// Represents a point coordinate in the [GoogleMap]'s view. /// @@ -28,19 +26,19 @@ class ScreenCoordinate { /// Converts this object to something serializable in JSON. Object toJson() { return { - "x": x, - "y": y, + 'x': x, + 'y': y, }; } @override - String toString() => '$runtimeType($x, $y)'; + String toString() => '${objectRuntimeType(this, 'ScreenCoordinate')}($x, $y)'; @override - bool operator ==(Object o) { - return o is ScreenCoordinate && o.x == x && o.y == y; + bool operator ==(Object other) { + return other is ScreenCoordinate && other.x == x && other.y == y; } @override - int get hashCode => hashValues(x, y); + int get hashCode => Object.hash(x, y); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart index db2b53d63512..aaf0f800f47f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart' show immutable; import 'types.dart'; @@ -43,8 +41,8 @@ class TileOverlayId extends MapsObjectId { /// The coordinates of the tiles are measured from the top left (northwest) corner of the map. /// At zoom level N, the x values of the tile coordinates range from 0 to 2N - 1 and increase from /// west to east and the y values range from 0 to 2N - 1 and increase from north to south. -/// -class TileOverlay implements MapsObject { +@immutable +class TileOverlay implements MapsObject { /// Creates an immutable representation of a [TileOverlay] to draw on [GoogleMap]. const TileOverlay({ required this.tileOverlayId, @@ -108,9 +106,11 @@ class TileOverlay implements MapsObject { ); } + @override TileOverlay clone() => copyWith(); /// Converts this object to JSON. + @override Object toJson() { final Map json = {}; @@ -146,6 +146,6 @@ class TileOverlay implements MapsObject { } @override - int get hashCode => hashValues(tileOverlayId, fadeIn, tileProvider, + int get hashCode => Object.hash(tileOverlayId, fadeIn, tileProvider, transparency, zIndex, visible, tileSize); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart index 5e2e4c234ccf..0beb7d747ec8 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart @@ -7,26 +7,28 @@ export 'bitmap.dart'; export 'callbacks.dart'; export 'camera.dart'; export 'cap.dart'; -export 'circle_updates.dart'; export 'circle.dart'; +export 'circle_updates.dart'; export 'joint_type.dart'; export 'location.dart'; -export 'maps_object_updates.dart'; +export 'map_configuration.dart'; +export 'map_objects.dart'; +export 'map_widget_configuration.dart'; export 'maps_object.dart'; -export 'marker_updates.dart'; +export 'maps_object_updates.dart'; export 'marker.dart'; +export 'marker_updates.dart'; export 'pattern_item.dart'; -export 'polygon_updates.dart'; export 'polygon.dart'; -export 'polyline_updates.dart'; +export 'polygon_updates.dart'; export 'polyline.dart'; +export 'polyline_updates.dart'; export 'screen_coordinate.dart'; export 'tile.dart'; export 'tile_overlay.dart'; export 'tile_provider.dart'; export 'ui.dart'; - -// Export the utils, they're used by the Widget +// Export the utils used by the Widget export 'utils/circle.dart'; export 'utils/marker.dart'; export 'utils/polygon.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart index 38c34fcfd27f..482f64be8b4f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; +import 'package:flutter/foundation.dart'; import 'types.dart'; @@ -31,6 +31,7 @@ enum MapType { // Used with [GoogleMapOptions] to wrap a [LatLngBounds] value. This allows // distinguishing between specifying an unbounded target (null `LatLngBounds`) // from not specifying anything (null `CameraTargetBounds`). +@immutable class CameraTargetBounds { /// Creates a camera target bounds with the specified bounding box, or null /// to indicate that the camera target is not bounded. @@ -49,10 +50,13 @@ class CameraTargetBounds { @override bool operator ==(Object other) { - if (identical(this, other)) return true; - if (runtimeType != other.runtimeType) return false; - final CameraTargetBounds typedOther = other as CameraTargetBounds; - return bounds == typedOther.bounds; + if (identical(this, other)) { + return true; + } + if (runtimeType != other.runtimeType) { + return false; + } + return other is CameraTargetBounds && bounds == other.bounds; } @override @@ -68,6 +72,7 @@ class CameraTargetBounds { // Used with [GoogleMapOptions] to wrap min and max zoom. This allows // distinguishing between specifying unbounded zooming (null `minZoom` and // `maxZoom`) from not specifying anything (null `MinMaxZoomPreference`). +@immutable class MinMaxZoomPreference { /// Creates a immutable representation of the preferred minimum and maximum zoom values for the map camera. /// @@ -90,14 +95,19 @@ class MinMaxZoomPreference { @override bool operator ==(Object other) { - if (identical(this, other)) return true; - if (runtimeType != other.runtimeType) return false; - final MinMaxZoomPreference typedOther = other as MinMaxZoomPreference; - return minZoom == typedOther.minZoom && maxZoom == typedOther.maxZoom; + if (identical(this, other)) { + return true; + } + if (runtimeType != other.runtimeType) { + return false; + } + return other is MinMaxZoomPreference && + minZoom == other.minZoom && + maxZoom == other.maxZoom; } @override - int get hashCode => hashValues(minZoom, maxZoom); + int get hashCode => Object.hash(minZoom, maxZoom); @override String toString() { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/map_configuration_serialization.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/map_configuration_serialization.dart new file mode 100644 index 000000000000..01f4fa054570 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/map_configuration_serialization.dart @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import '../map_configuration.dart'; + +/// Returns a JSON representation of [config]. +/// +/// This is intended for two purposes: +/// - Conversion of [MapConfiguration] to the map options dictionary used by +/// legacy platform interface methods. +/// - Conversion of [MapConfiguration] to the default method channel +/// implementation's representation. +/// +/// Both of these are parts of the public interface, so any change to the +/// representation other than adding a new field requires a breaking change to +/// the package. +Map jsonForMapConfiguration(MapConfiguration config) { + final EdgeInsets? padding = config.padding; + return { + if (config.compassEnabled != null) 'compassEnabled': config.compassEnabled!, + if (config.mapToolbarEnabled != null) + 'mapToolbarEnabled': config.mapToolbarEnabled!, + if (config.cameraTargetBounds != null) + 'cameraTargetBounds': config.cameraTargetBounds!.toJson(), + if (config.mapType != null) 'mapType': config.mapType!.index, + if (config.minMaxZoomPreference != null) + 'minMaxZoomPreference': config.minMaxZoomPreference!.toJson(), + if (config.rotateGesturesEnabled != null) + 'rotateGesturesEnabled': config.rotateGesturesEnabled!, + if (config.scrollGesturesEnabled != null) + 'scrollGesturesEnabled': config.scrollGesturesEnabled!, + if (config.tiltGesturesEnabled != null) + 'tiltGesturesEnabled': config.tiltGesturesEnabled!, + if (config.zoomControlsEnabled != null) + 'zoomControlsEnabled': config.zoomControlsEnabled!, + if (config.zoomGesturesEnabled != null) + 'zoomGesturesEnabled': config.zoomGesturesEnabled!, + if (config.liteModeEnabled != null) + 'liteModeEnabled': config.liteModeEnabled!, + if (config.trackCameraPosition != null) + 'trackCameraPosition': config.trackCameraPosition!, + if (config.myLocationEnabled != null) + 'myLocationEnabled': config.myLocationEnabled!, + if (config.myLocationButtonEnabled != null) + 'myLocationButtonEnabled': config.myLocationButtonEnabled!, + if (padding != null) + 'padding': [ + padding.top, + padding.left, + padding.bottom, + padding.right, + ], + if (config.indoorViewEnabled != null) + 'indoorEnabled': config.indoorViewEnabled!, + if (config.trafficEnabled != null) 'trafficEnabled': config.trafficEnabled!, + if (config.buildingsEnabled != null) + 'buildingsEnabled': config.buildingsEnabled!, + }; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/maps_object.dart index da5a49825c7f..d17dbd279dfe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/maps_object.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/maps_object.dart @@ -5,14 +5,13 @@ import '../maps_object.dart'; /// Converts an [Iterable] of [MapsObject]s in a Map of [MapObjectId] -> [MapObject]. -Map, T> keyByMapsObjectId( +Map, T> keyByMapsObjectId>( Iterable objects) { return Map, T>.fromEntries(objects.map((T object) => - MapEntry, T>( - object.mapsId as MapsObjectId, object.clone()))); + MapEntry, T>(object.mapsId, object.clone()))); } /// Converts a Set of [MapsObject]s into something serializable in JSON. -Object serializeMapsObjectSet(Set mapsObjects) { - return mapsObjects.map((MapsObject p) => p.toJson()).toList(); +Object serializeMapsObjectSet(Set> mapsObjects) { + return mapsObjects.map((MapsObject p) => p.toJson()).toList(); } 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 6125dd43d9f6..5639ee8c6ad7 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 @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_fl issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.1.5 +version: 2.2.4 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.0.0" + flutter: ">=2.10.0" dependencies: collection: ^1.15.0 @@ -22,4 +22,3 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.0 - pedantic: ^1.10.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 176f702ff0ff..e5052184915f 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 @@ -2,15 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:async/async.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import 'package:google_maps_flutter_platform_interface/src/events/map_event.dart'; -import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'dart:async'; - -import 'package:async/async.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -42,8 +37,8 @@ void main() { final ByteData byteData = const StandardMethodCodec() .encodeMethodCall(MethodCall(method, data)); await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger - .handlePlatformMessage( - "plugins.flutter.io/google_maps_$mapId", byteData, (data) {}); + .handlePlatformMessage('plugins.flutter.io/google_maps_$mapId', + byteData, (ByteData? data) {}); } // Calls each method that uses invokeMethod with a return type other than @@ -69,8 +64,8 @@ void main() { } }); - await maps.getLatLng(ScreenCoordinate(x: 0, y: 0), mapId: mapId); - await maps.isMarkerInfoWindowShown(MarkerId(''), mapId: mapId); + await maps.getLatLng(const ScreenCoordinate(x: 0, y: 0), mapId: mapId); + await maps.isMarkerInfoWindowShown(const MarkerId(''), mapId: mapId); await maps.getZoomLevel(mapId: mapId); await maps.takeSnapshot(mapId: mapId); // Check that all the invokeMethod calls happened. @@ -83,20 +78,20 @@ void main() { }); test('markers send drag event to correct streams', () async { const int mapId = 1; - final jsonMarkerDragStartEvent = { - "mapId": mapId, - "markerId": "drag-start-marker", - "position": [1.0, 1.0] + final Map jsonMarkerDragStartEvent = { + 'mapId': mapId, + 'markerId': 'drag-start-marker', + 'position': [1.0, 1.0] }; - final jsonMarkerDragEvent = { - "mapId": mapId, - "markerId": "drag-marker", - "position": [1.0, 1.0] + final Map jsonMarkerDragEvent = { + 'mapId': mapId, + 'markerId': 'drag-marker', + 'position': [1.0, 1.0] }; - final jsonMarkerDragEndEvent = { - "mapId": mapId, - "markerId": "drag-end-marker", - "position": [1.0, 1.0] + final Map jsonMarkerDragEndEvent = { + 'mapId': mapId, + 'markerId': 'drag-end-marker', + 'position': [1.0, 1.0] }; final MethodChannelGoogleMapsFlutter maps = @@ -104,23 +99,24 @@ void main() { maps.ensureChannelInitialized(mapId); final StreamQueue markerDragStartStream = - StreamQueue(maps.onMarkerDragStart(mapId: mapId)); + StreamQueue( + maps.onMarkerDragStart(mapId: mapId)); final StreamQueue markerDragStream = - StreamQueue(maps.onMarkerDrag(mapId: mapId)); + StreamQueue(maps.onMarkerDrag(mapId: mapId)); final StreamQueue markerDragEndStream = - StreamQueue(maps.onMarkerDragEnd(mapId: mapId)); + StreamQueue(maps.onMarkerDragEnd(mapId: mapId)); await sendPlatformMessage( - mapId, "marker#onDragStart", jsonMarkerDragStartEvent); - await sendPlatformMessage(mapId, "marker#onDrag", jsonMarkerDragEvent); + mapId, 'marker#onDragStart', jsonMarkerDragStartEvent); + await sendPlatformMessage(mapId, 'marker#onDrag', jsonMarkerDragEvent); await sendPlatformMessage( - mapId, "marker#onDragEnd", jsonMarkerDragEndEvent); + mapId, 'marker#onDragEnd', jsonMarkerDragEndEvent); expect((await markerDragStartStream.next).value.value, - equals("drag-start-marker")); - expect((await markerDragStream.next).value.value, equals("drag-marker")); + equals('drag-start-marker')); + expect((await markerDragStream.next).value.value, equals('drag-marker')); expect((await markerDragEndStream.next).value.value, - equals("drag-end-marker")); + equals('drag-end-marker')); }); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart index c381f9e30750..d1dba2b75b55 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart @@ -6,12 +6,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; -import 'package:mockito/mockito.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -29,7 +27,14 @@ void main() { expect(() { GoogleMapsFlutterPlatform.instance = ImplementsGoogleMapsFlutterPlatform(); - }, throwsA(isInstanceOf())); + // In versions of `package:plugin_platform_interface` prior to fixing + // https://github.com/flutter/flutter/issues/109339, an attempt to + // implement a platform interface using `implements` would sometimes + // throw a `NoSuchMethodError` and other times throw an + // `AssertionError`. After the issue is fixed, an `AssertionError` will + // always be thrown. For the purpose of this test, we don't really care + // what exception is thrown, so just allow any exception. + }, throwsA(anything)); }); test('Can be mocked with `implements`', () { @@ -51,13 +56,33 @@ void main() { platform.buildViewWithTextDirection( 0, (_) {}, - initialCameraPosition: CameraPosition(target: LatLng(0.0, 0.0)), + initialCameraPosition: + const CameraPosition(target: LatLng(0.0, 0.0)), textDirection: TextDirection.ltr, ), isA(), ); }, ); + + test( + 'default implementation of `buildViewWithConfiguration` delegates to `buildViewWithTextDirection`', + () { + final GoogleMapsFlutterPlatform platform = + BuildViewGoogleMapsFlutterPlatform(); + expect( + platform.buildViewWithConfiguration( + 0, + (_) {}, + widgetConfiguration: const MapWidgetConfiguration( + initialCameraPosition: CameraPosition(target: LatLng(0.0, 0.0)), + textDirection: TextDirection.ltr, + ), + ), + isA(), + ); + }, + ); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_inspector_platform_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_inspector_platform_test.dart new file mode 100644 index 000000000000..689289b6ffde --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_inspector_platform_test.dart @@ -0,0 +1,33 @@ +// 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_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:mockito/mockito.dart'; + +void main() { + // Store the initial instance before any tests change it. + final GoogleMapsInspectorPlatform? initialInstance = + GoogleMapsInspectorPlatform.instance; + + test('default instance is null', () { + expect(initialInstance, isNull); + }); + + test('cannot be implemented with `implements`', () { + expect(() { + GoogleMapsInspectorPlatform.instance = + ImplementsGoogleMapsInspectorPlatform(); + }, throwsA(isInstanceOf())); + }); + + test('can be implement with `extends`', () { + GoogleMapsInspectorPlatform.instance = ExtendsGoogleMapsInspectorPlatform(); + }); +} + +class ImplementsGoogleMapsInspectorPlatform extends Mock + implements GoogleMapsInspectorPlatform {} + +class ExtendsGoogleMapsInspectorPlatform extends GoogleMapsInspectorPlatform {} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart index 6d02b2c630df..2499e87bb649 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart @@ -2,8 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore:unnecessary_import import 'dart:typed_data'; +import 'dart:ui'; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; @@ -13,96 +16,151 @@ void main() { group('$BitmapDescriptor', () { test('toJson / fromJson', () { - final descriptor = + final BitmapDescriptor descriptor = BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); - final json = descriptor.toJson(); + final Object json = descriptor.toJson(); // Rehydrate a new bitmap descriptor... // ignore: deprecated_member_use_from_same_package - final descriptorFromJson = BitmapDescriptor.fromJson(json); + final BitmapDescriptor descriptorFromJson = + BitmapDescriptor.fromJson(json); expect(descriptorFromJson, isNot(descriptor)); // New instance expect(identical(descriptorFromJson.toJson(), json), isTrue); // Same JSON }); + group('fromBytes constructor', () { + test('with empty byte array, throws assertion error', () { + expect(() { + BitmapDescriptor.fromBytes(Uint8List.fromList([])); + }, throwsAssertionError); + }); + + test('with bytes', () { + final BitmapDescriptor descriptor = BitmapDescriptor.fromBytes( + Uint8List.fromList([1, 2, 3]), + ); + expect(descriptor, isA()); + expect( + descriptor.toJson(), + equals([ + 'fromBytes', + [1, 2, 3], + ])); + }); + + test('with size, not on the web, size is ignored', () { + final BitmapDescriptor descriptor = BitmapDescriptor.fromBytes( + Uint8List.fromList([1, 2, 3]), + size: const Size(40, 20), + ); + + expect( + descriptor.toJson(), + equals([ + 'fromBytes', + [1, 2, 3], + ])); + }, skip: kIsWeb); + + test('with size, on the web, size is preserved', () { + final BitmapDescriptor descriptor = BitmapDescriptor.fromBytes( + Uint8List.fromList([1, 2, 3]), + size: const Size(40, 20), + ); + + expect( + descriptor.toJson(), + equals([ + 'fromBytes', + [1, 2, 3], + [40, 20], + ])); + }, skip: !kIsWeb); + }); + group('fromJson validation', () { group('type validation', () { test('correct type', () { - expect(BitmapDescriptor.fromJson(['defaultMarker']), + expect(BitmapDescriptor.fromJson(['defaultMarker']), isA()); }); test('wrong type', () { expect(() { - BitmapDescriptor.fromJson(['bogusType']); + BitmapDescriptor.fromJson(['bogusType']); }, throwsAssertionError); }); }); group('defaultMarker', () { test('hue is null', () { - expect(BitmapDescriptor.fromJson(['defaultMarker']), + expect(BitmapDescriptor.fromJson(['defaultMarker']), isA()); }); test('hue is number', () { - expect(BitmapDescriptor.fromJson(['defaultMarker', 158]), + expect(BitmapDescriptor.fromJson(['defaultMarker', 158]), isA()); }); test('hue is not number', () { expect(() { - BitmapDescriptor.fromJson(['defaultMarker', 'nope']); + BitmapDescriptor.fromJson(['defaultMarker', 'nope']); }, throwsAssertionError); }); test('hue is out of range', () { expect(() { - BitmapDescriptor.fromJson(['defaultMarker', -1]); + BitmapDescriptor.fromJson(['defaultMarker', -1]); }, throwsAssertionError); expect(() { - BitmapDescriptor.fromJson(['defaultMarker', 361]); + BitmapDescriptor.fromJson(['defaultMarker', 361]); }, throwsAssertionError); }); }); group('fromBytes', () { test('with bytes', () { expect( - BitmapDescriptor.fromJson([ + BitmapDescriptor.fromJson([ 'fromBytes', - Uint8List.fromList([1, 2, 3]) + Uint8List.fromList([1, 2, 3]) ]), isA()); }); test('without bytes', () { expect(() { - BitmapDescriptor.fromJson(['fromBytes', null]); + BitmapDescriptor.fromJson(['fromBytes', null]); }, throwsAssertionError); expect(() { - BitmapDescriptor.fromJson(['fromBytes', []]); + BitmapDescriptor.fromJson(['fromBytes', []]); }, throwsAssertionError); }); }); group('fromAsset', () { test('name is passed', () { - expect(BitmapDescriptor.fromJson(['fromAsset', 'some/path.png']), + expect( + BitmapDescriptor.fromJson( + ['fromAsset', 'some/path.png']), isA()); }); test('name cannot be null or empty', () { expect(() { - BitmapDescriptor.fromJson(['fromAsset', null]); + BitmapDescriptor.fromJson(['fromAsset', null]); }, throwsAssertionError); expect(() { - BitmapDescriptor.fromJson(['fromAsset', '']); + BitmapDescriptor.fromJson(['fromAsset', '']); }, throwsAssertionError); }); test('package is passed', () { expect( BitmapDescriptor.fromJson( - ['fromAsset', 'some/path.png', 'some_package']), + ['fromAsset', 'some/path.png', 'some_package']), isA()); }); test('package cannot be null or empty', () { expect(() { - BitmapDescriptor.fromJson(['fromAsset', 'some/path.png', null]); + BitmapDescriptor.fromJson( + ['fromAsset', 'some/path.png', null]); }, throwsAssertionError); expect(() { - BitmapDescriptor.fromJson(['fromAsset', 'some/path.png', '']); + BitmapDescriptor.fromJson( + ['fromAsset', 'some/path.png', '']); }, throwsAssertionError); }); }); @@ -110,34 +168,34 @@ void main() { test('name and dpi passed', () { expect( BitmapDescriptor.fromJson( - ['fromAssetImage', 'some/path.png', 1.0]), + ['fromAssetImage', 'some/path.png', 1.0]), isA()); }); test('name cannot be null or empty', () { expect(() { - BitmapDescriptor.fromJson(['fromAssetImage', null, 1.0]); + BitmapDescriptor.fromJson(['fromAssetImage', null, 1.0]); }, throwsAssertionError); expect(() { - BitmapDescriptor.fromJson(['fromAssetImage', '', 1.0]); + BitmapDescriptor.fromJson(['fromAssetImage', '', 1.0]); }, throwsAssertionError); }); test('dpi must be number', () { expect(() { BitmapDescriptor.fromJson( - ['fromAssetImage', 'some/path.png', null]); + ['fromAssetImage', 'some/path.png', null]); }, throwsAssertionError); expect(() { BitmapDescriptor.fromJson( - ['fromAssetImage', 'some/path.png', 'one']); + ['fromAssetImage', 'some/path.png', 'one']); }, throwsAssertionError); }); test('with optional [width, height] List', () { expect( - BitmapDescriptor.fromJson([ + BitmapDescriptor.fromJson([ 'fromAssetImage', 'some/path.png', 1.0, - [640, 480] + [640, 480] ]), isA()); }); @@ -146,18 +204,18 @@ void main() { () { expect(() { BitmapDescriptor.fromJson( - ['fromAssetImage', 'some/path.png', 1.0, null]); + ['fromAssetImage', 'some/path.png', 1.0, null]); }, throwsAssertionError); expect(() { BitmapDescriptor.fromJson( - ['fromAssetImage', 'some/path.png', 1.0, []]); + ['fromAssetImage', 'some/path.png', 1.0, []]); }, throwsAssertionError); expect(() { - BitmapDescriptor.fromJson([ + BitmapDescriptor.fromJson([ 'fromAssetImage', 'some/path.png', 1.0, - [640, 480, 1024] + [640, 480, 1024] ]); }, throwsAssertionError); }); diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/camera_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/camera_test.dart index 11665d904556..70e57aa67ac9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/camera_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/camera_test.dart @@ -9,13 +9,14 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('toMap / fromMap', () { - const cameraPosition = CameraPosition( + const CameraPosition cameraPosition = CameraPosition( target: LatLng(10.0, 15.0), bearing: 0.5, tilt: 30.0, zoom: 1.5); // Cast to to ensure that recreating from JSON, where // type information will have likely been lost, still works. - final json = (cameraPosition.toMap() as Map) - .cast(); - final cameraPositionFromJson = CameraPosition.fromMap(json); + final Map json = + (cameraPosition.toMap() as Map) + .cast(); + final CameraPosition? cameraPositionFromJson = CameraPosition.fromMap(json); expect(cameraPosition, cameraPositionFromJson); }); diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/location_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/location_test.dart index 80f696177dfd..9da3e543ea58 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/location_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/location_test.dart @@ -10,50 +10,50 @@ void main() { group('LanLng constructor', () { test('Maintains longitude precision if within acceptable range', () async { - const lat = -34.509981; - const lng = 150.792384; + const double lat = -34.509981; + const double lng = 150.792384; - final latLng = LatLng(lat, lng); + const LatLng latLng = LatLng(lat, lng); expect(latLng.latitude, equals(lat)); expect(latLng.longitude, equals(lng)); }); test('Normalizes longitude that is below lower limit', () async { - const lat = -34.509981; - const lng = -270.0; + const double lat = -34.509981; + const double lng = -270.0; - final latLng = LatLng(lat, lng); + const LatLng latLng = LatLng(lat, lng); expect(latLng.latitude, equals(lat)); expect(latLng.longitude, equals(90.0)); }); test('Normalizes longitude that is above upper limit', () async { - const lat = -34.509981; - const lng = 270.0; + const double lat = -34.509981; + const double lng = 270.0; - final latLng = LatLng(lat, lng); + const LatLng latLng = LatLng(lat, lng); expect(latLng.latitude, equals(lat)); expect(latLng.longitude, equals(-90.0)); }); test('Includes longitude set to lower limit', () async { - const lat = -34.509981; - const lng = -180.0; + const double lat = -34.509981; + const double lng = -180.0; - final latLng = LatLng(lat, lng); + const LatLng latLng = LatLng(lat, lng); expect(latLng.latitude, equals(lat)); expect(latLng.longitude, equals(-180.0)); }); test('Normalizes longitude set to upper limit', () async { - const lat = -34.509981; - const lng = 180.0; + const double lat = -34.509981; + const double lng = 180.0; - final latLng = LatLng(lat, lng); + const LatLng latLng = LatLng(lat, lng); expect(latLng.latitude, equals(lat)); expect(latLng.longitude, equals(-180.0)); diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/map_configuration_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/map_configuration_test.dart new file mode 100644 index 000000000000..edd1fd091073 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/map_configuration_test.dart @@ -0,0 +1,412 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + group('diffs', () { + // A options instance with every field set, to test diffs against. + final MapConfiguration diffBase = MapConfiguration( + compassEnabled: false, + mapToolbarEnabled: false, + cameraTargetBounds: CameraTargetBounds(LatLngBounds( + northeast: const LatLng(30, 20), southwest: const LatLng(10, 40))), + mapType: MapType.normal, + minMaxZoomPreference: const MinMaxZoomPreference(1.0, 10.0), + rotateGesturesEnabled: false, + scrollGesturesEnabled: false, + tiltGesturesEnabled: false, + trackCameraPosition: false, + zoomControlsEnabled: false, + zoomGesturesEnabled: false, + liteModeEnabled: false, + myLocationEnabled: false, + myLocationButtonEnabled: false, + padding: const EdgeInsets.all(5.0), + indoorViewEnabled: false, + trafficEnabled: false, + buildingsEnabled: false, + ); + + test('only include changed fields', () async { + const MapConfiguration nullOptions = MapConfiguration(); + + // Everything should be null since nothing changed. + expect(diffBase.diffFrom(diffBase), nullOptions); + }); + + test('only apply non-null fields', () async { + const MapConfiguration smallDiff = MapConfiguration(compassEnabled: true); + + final MapConfiguration updated = diffBase.applyDiff(smallDiff); + + // The diff should be updated. + expect(updated.compassEnabled, true); + // Spot check that other fields weren't stomped. + expect(updated.mapToolbarEnabled, isNot(null)); + expect(updated.cameraTargetBounds, isNot(null)); + expect(updated.mapType, isNot(null)); + expect(updated.zoomControlsEnabled, isNot(null)); + expect(updated.liteModeEnabled, isNot(null)); + expect(updated.padding, isNot(null)); + expect(updated.trafficEnabled, isNot(null)); + }); + + test('handle compassEnabled', () async { + const MapConfiguration diff = MapConfiguration(compassEnabled: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.compassEnabled, true); + }); + + test('handle mapToolbarEnabled', () async { + const MapConfiguration diff = MapConfiguration(mapToolbarEnabled: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.mapToolbarEnabled, true); + }); + + test('handle cameraTargetBounds', () async { + final CameraTargetBounds newBounds = CameraTargetBounds(LatLngBounds( + northeast: const LatLng(55, 15), southwest: const LatLng(5, 15))); + final MapConfiguration diff = + MapConfiguration(cameraTargetBounds: newBounds); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.cameraTargetBounds, newBounds); + }); + + test('handle mapType', () async { + const MapConfiguration diff = + MapConfiguration(mapType: MapType.satellite); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.mapType, MapType.satellite); + }); + + test('handle minMaxZoomPreference', () async { + const MinMaxZoomPreference newZoomPref = MinMaxZoomPreference(3.3, 4.5); + const MapConfiguration diff = + MapConfiguration(minMaxZoomPreference: newZoomPref); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.minMaxZoomPreference, newZoomPref); + }); + + test('handle rotateGesturesEnabled', () async { + const MapConfiguration diff = + MapConfiguration(rotateGesturesEnabled: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.rotateGesturesEnabled, true); + }); + + test('handle scrollGesturesEnabled', () async { + const MapConfiguration diff = + MapConfiguration(scrollGesturesEnabled: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.scrollGesturesEnabled, true); + }); + + test('handle tiltGesturesEnabled', () async { + const MapConfiguration diff = MapConfiguration(tiltGesturesEnabled: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.tiltGesturesEnabled, true); + }); + + test('handle trackCameraPosition', () async { + const MapConfiguration diff = MapConfiguration(trackCameraPosition: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.trackCameraPosition, true); + }); + + test('handle zoomControlsEnabled', () async { + const MapConfiguration diff = MapConfiguration(zoomControlsEnabled: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.zoomControlsEnabled, true); + }); + + test('handle zoomGesturesEnabled', () async { + const MapConfiguration diff = MapConfiguration(zoomGesturesEnabled: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.zoomGesturesEnabled, true); + }); + + test('handle liteModeEnabled', () async { + const MapConfiguration diff = MapConfiguration(liteModeEnabled: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.liteModeEnabled, true); + }); + + test('handle myLocationEnabled', () async { + const MapConfiguration diff = MapConfiguration(myLocationEnabled: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.myLocationEnabled, true); + }); + + test('handle myLocationButtonEnabled', () async { + const MapConfiguration diff = + MapConfiguration(myLocationButtonEnabled: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.myLocationButtonEnabled, true); + }); + + test('handle padding', () async { + const EdgeInsets newPadding = + EdgeInsets.symmetric(vertical: 1.0, horizontal: 3.0); + const MapConfiguration diff = MapConfiguration(padding: newPadding); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.padding, newPadding); + }); + + test('handle indoorViewEnabled', () async { + const MapConfiguration diff = MapConfiguration(indoorViewEnabled: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.indoorViewEnabled, true); + }); + + test('handle trafficEnabled', () async { + const MapConfiguration diff = MapConfiguration(trafficEnabled: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.trafficEnabled, true); + }); + + test('handle buildingsEnabled', () async { + const MapConfiguration diff = MapConfiguration(buildingsEnabled: true); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // A diff applied to non-empty options should update that field. + expect(updated.buildingsEnabled, true); + }); + }); + + group('isEmpty', () { + test('is true for empty', () async { + const MapConfiguration nullOptions = MapConfiguration(); + + expect(nullOptions.isEmpty, true); + }); + + test('is false with compassEnabled', () async { + const MapConfiguration diff = MapConfiguration(compassEnabled: true); + + expect(diff.isEmpty, false); + }); + + test('is false with mapToolbarEnabled', () async { + const MapConfiguration diff = MapConfiguration(mapToolbarEnabled: true); + + expect(diff.isEmpty, false); + }); + + test('is false with cameraTargetBounds', () async { + final CameraTargetBounds newBounds = CameraTargetBounds(LatLngBounds( + northeast: const LatLng(55, 15), southwest: const LatLng(5, 15))); + final MapConfiguration diff = + MapConfiguration(cameraTargetBounds: newBounds); + + expect(diff.isEmpty, false); + }); + + test('is false with mapType', () async { + const MapConfiguration diff = + MapConfiguration(mapType: MapType.satellite); + + expect(diff.isEmpty, false); + }); + + test('is false with minMaxZoomPreference', () async { + const MinMaxZoomPreference newZoomPref = MinMaxZoomPreference(3.3, 4.5); + const MapConfiguration diff = + MapConfiguration(minMaxZoomPreference: newZoomPref); + + expect(diff.isEmpty, false); + }); + + test('is false with rotateGesturesEnabled', () async { + const MapConfiguration diff = + MapConfiguration(rotateGesturesEnabled: true); + + expect(diff.isEmpty, false); + }); + + test('is false with scrollGesturesEnabled', () async { + const MapConfiguration diff = + MapConfiguration(scrollGesturesEnabled: true); + + expect(diff.isEmpty, false); + }); + + test('is false with tiltGesturesEnabled', () async { + const MapConfiguration diff = MapConfiguration(tiltGesturesEnabled: true); + + expect(diff.isEmpty, false); + }); + + test('is false with trackCameraPosition', () async { + const MapConfiguration diff = MapConfiguration(trackCameraPosition: true); + + expect(diff.isEmpty, false); + }); + + test('is false with zoomControlsEnabled', () async { + const MapConfiguration diff = MapConfiguration(zoomControlsEnabled: true); + + expect(diff.isEmpty, false); + }); + + test('is false with zoomGesturesEnabled', () async { + const MapConfiguration diff = MapConfiguration(zoomGesturesEnabled: true); + + expect(diff.isEmpty, false); + }); + + test('is false with liteModeEnabled', () async { + const MapConfiguration diff = MapConfiguration(liteModeEnabled: true); + + expect(diff.isEmpty, false); + }); + + test('is false with myLocationEnabled', () async { + const MapConfiguration diff = MapConfiguration(myLocationEnabled: true); + + expect(diff.isEmpty, false); + }); + + test('is false with myLocationButtonEnabled', () async { + const MapConfiguration diff = + MapConfiguration(myLocationButtonEnabled: true); + + expect(diff.isEmpty, false); + }); + + test('is false with padding', () async { + const EdgeInsets newPadding = + EdgeInsets.symmetric(vertical: 1.0, horizontal: 3.0); + const MapConfiguration diff = MapConfiguration(padding: newPadding); + + expect(diff.isEmpty, false); + }); + + test('is false with indoorViewEnabled', () async { + const MapConfiguration diff = MapConfiguration(indoorViewEnabled: true); + + expect(diff.isEmpty, false); + }); + + test('is false with trafficEnabled', () async { + const MapConfiguration diff = MapConfiguration(trafficEnabled: true); + + expect(diff.isEmpty, false); + }); + + test('is false with buildingsEnabled', () async { + const MapConfiguration diff = MapConfiguration(buildingsEnabled: true); + + expect(diff.isEmpty, false); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_test.dart index c2ca2bdda5b7..7c5106c23173 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_test.dart @@ -37,9 +37,9 @@ void main() { expect( serializeMapsObjectSet({object1, object2, object3}), >[ - {'id': '1'}, - {'id': '2'}, - {'id': '3'} + {'id': '1'}, + {'id': '2'}, + {'id': '3'} ]); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart index f09f70fd769e..414196b8333c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues, hashList; - -import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter_platform_interface/src/types/maps_object.dart'; import 'package:google_maps_flutter_platform_interface/src/types/maps_object_updates.dart'; @@ -33,24 +30,25 @@ void main() { TestMapsObject(MapsObjectId('id3'), data: 2); const TestMapsObject to4 = TestMapsObject(MapsObjectId('id4')); - final Set previous = - Set.from([to1, to2, to3]); - final Set current = - Set.from([to2, to3Changed, to4]); + final Set previous = {to1, to2, to3}; + final Set current = { + to2, + to3Changed, + to4 + }; final TestMapsObjectUpdate updates = TestMapsObjectUpdate.from(previous, current); final Set> toRemove = - Set.from(>[ + >{ const MapsObjectId('id1') - ]); + }; expect(updates.objectIdsToRemove, toRemove); - final Set toAdd = Set.from([to4]); + final Set toAdd = {to4}; expect(updates.objectsToAdd, toAdd); - final Set toChange = - Set.from([to3Changed]); + final Set toChange = {to3Changed}; expect(updates.objectsToChange, toChange); }); @@ -65,10 +63,12 @@ void main() { TestMapsObject(MapsObjectId('id3'), data: 2); const TestMapsObject to4 = TestMapsObject(MapsObjectId('id4')); - final Set previous = - Set.from([to1, to2, to3]); - final Set current = - Set.from([to2, to3Changed, to4]); + final Set previous = {to1, to2, to3}; + final Set current = { + to2, + to3Changed, + to4 + }; final TestMapsObjectUpdate updates = TestMapsObjectUpdate.from(previous, current); @@ -93,13 +93,18 @@ void main() { TestMapsObject(MapsObjectId('id3'), data: 2); const TestMapsObject to4 = TestMapsObject(MapsObjectId('id4')); - final Set previous = - Set.from([to1, to2, to3]); - final Set current1 = - Set.from([to2, to3Changed, to4]); - final Set current2 = - Set.from([to2, to3Changed, to4]); - final Set current3 = Set.from([to2, to4]); + final Set previous = {to1, to2, to3}; + final Set current1 = { + to2, + to3Changed, + to4 + }; + final Set current2 = { + to2, + to3Changed, + to4 + }; + final Set current3 = {to2, to4}; final TestMapsObjectUpdate updates1 = TestMapsObjectUpdate.from(previous, current1); final TestMapsObjectUpdate updates2 = @@ -121,18 +126,20 @@ void main() { TestMapsObject(MapsObjectId('id3'), data: 2); const TestMapsObject to4 = TestMapsObject(MapsObjectId('id4')); - final Set previous = - Set.from([to1, to2, to3]); - final Set current = - Set.from([to2, to3Changed, to4]); + final Set previous = {to1, to2, to3}; + final Set current = { + to2, + to3Changed, + to4 + }; final TestMapsObjectUpdate updates = TestMapsObjectUpdate.from(previous, current); expect( updates.hashCode, - hashValues( - hashList(updates.objectsToAdd), - hashList(updates.objectIdsToRemove), - hashList(updates.objectsToChange))); + Object.hash( + Object.hashAll(updates.objectsToAdd), + Object.hashAll(updates.objectIdsToRemove), + Object.hashAll(updates.objectsToChange))); }); test('toString', () async { @@ -146,10 +153,12 @@ void main() { TestMapsObject(MapsObjectId('id3'), data: 2); const TestMapsObject to4 = TestMapsObject(MapsObjectId('id4')); - final Set previous = - Set.from([to1, to2, to3]); - final Set current = - Set.from([to2, to3Changed, to4]); + final Set previous = {to1, to2, to3}; + final Set current = { + to2, + to3Changed, + to4 + }; final TestMapsObjectUpdate updates = TestMapsObjectUpdate.from(previous, current); expect( diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.dart index c8f6fa527a95..db7afcbb0398 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; @@ -12,7 +11,7 @@ void main() { group('$Marker', () { test('constructor defaults', () { - final Marker marker = Marker(markerId: MarkerId("ABC123")); + const Marker marker = Marker(markerId: MarkerId('ABC123')); expect(marker.alpha, equals(1.0)); expect(marker.anchor, equals(const Offset(0.5, 1.0))); @@ -31,9 +30,10 @@ void main() { expect(marker.onDragEnd, equals(null)); }); test('constructor alpha is >= 0.0 and <= 1.0', () { - final ValueSetter initWithAlpha = (double alpha) { - Marker(markerId: MarkerId("ABC123"), alpha: alpha); - }; + void initWithAlpha(double alpha) { + Marker(markerId: const MarkerId('ABC123'), alpha: alpha); + } + expect(() => initWithAlpha(-0.5), throwsAssertionError); expect(() => initWithAlpha(0.0), isNot(throwsAssertionError)); expect(() => initWithAlpha(0.5), isNot(throwsAssertionError)); @@ -45,19 +45,19 @@ void main() { final BitmapDescriptor testDescriptor = BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); final Marker marker = Marker( - markerId: MarkerId("ABC123"), + markerId: const MarkerId('ABC123'), alpha: 0.12345, - anchor: Offset(100, 100), + anchor: const Offset(100, 100), consumeTapEvents: true, draggable: true, flat: true, icon: testDescriptor, - infoWindow: InfoWindow( - title: "Test title", - snippet: "Test snippet", + infoWindow: const InfoWindow( + title: 'Test title', + snippet: 'Test snippet', anchor: Offset(100, 200), ), - position: LatLng(50, 50), + position: const LatLng(50, 50), rotation: 100, visible: false, zIndex: 100, @@ -70,7 +70,7 @@ void main() { final Map json = marker.toJson() as Map; expect(json, { - 'markerId': "ABC123", + 'markerId': 'ABC123', 'alpha': 0.12345, 'anchor': [100, 100], 'consumeTapEvents': true, @@ -78,8 +78,8 @@ void main() { 'flat': true, 'icon': testDescriptor.toJson(), 'infoWindow': { - 'title': "Test title", - 'snippet': "Test snippet", + 'title': 'Test title', + 'snippet': 'Test snippet', 'anchor': [100.0, 200.0], }, 'position': [50, 50], @@ -89,31 +89,31 @@ void main() { }); }); test('clone', () { - final Marker marker = Marker(markerId: MarkerId("ABC123")); + const Marker marker = Marker(markerId: MarkerId('ABC123')); final Marker clone = marker.clone(); expect(identical(clone, marker), isFalse); expect(clone, equals(marker)); }); test('copyWith', () { - final Marker marker = Marker(markerId: MarkerId("ABC123")); + const Marker marker = Marker(markerId: MarkerId('ABC123')); final BitmapDescriptor testDescriptor = BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); - final double testAlphaParam = 0.12345; - final Offset testAnchorParam = Offset(100, 100); + const double testAlphaParam = 0.12345; + const Offset testAnchorParam = Offset(100, 100); final bool testConsumeTapEventsParam = !marker.consumeTapEvents; final bool testDraggableParam = !marker.draggable; final bool testFlatParam = !marker.flat; final BitmapDescriptor testIconParam = testDescriptor; - final InfoWindow testInfoWindowParam = InfoWindow(title: "Test"); - final LatLng testPositionParam = LatLng(100, 100); - final double testRotationParam = 100; + const InfoWindow testInfoWindowParam = InfoWindow(title: 'Test'); + const LatLng testPositionParam = LatLng(100, 100); + const double testRotationParam = 100; final bool testVisibleParam = !marker.visible; - final double testZIndexParam = 100; - final List log = []; + const double testZIndexParam = 100; + final List log = []; - final copy = marker.copyWith( + final Marker copy = marker.copyWith( alphaParam: testAlphaParam, anchorParam: testAnchorParam, consumeTapEventsParam: testConsumeTapEventsParam, @@ -126,16 +126,16 @@ void main() { visibleParam: testVisibleParam, zIndexParam: testZIndexParam, onTapParam: () { - log.add("onTapParam"); + log.add('onTapParam'); }, onDragStartParam: (LatLng latLng) { - log.add("onDragStartParam"); + log.add('onDragStartParam'); }, onDragParam: (LatLng latLng) { - log.add("onDragParam"); + log.add('onDragParam'); }, onDragEndParam: (LatLng latLng) { - log.add("onDragEndParam"); + log.add('onDragEndParam'); }, ); @@ -152,16 +152,16 @@ void main() { expect(copy.zIndex, equals(testZIndexParam)); copy.onTap!(); - expect(log, contains("onTapParam")); + expect(log, contains('onTapParam')); - copy.onDragStart!(LatLng(0, 1)); - expect(log, contains("onDragStartParam")); + copy.onDragStart!(const LatLng(0, 1)); + expect(log, contains('onDragStartParam')); - copy.onDrag!(LatLng(0, 1)); - expect(log, contains("onDragParam")); + copy.onDrag!(const LatLng(0, 1)); + expect(log, contains('onDragParam')); - copy.onDragEnd!(LatLng(0, 1)); - expect(log, contains("onDragEndParam")); + copy.onDragEnd!(const LatLng(0, 1)); + expect(log, contains('onDragEndParam')); }); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart index b95ae50a8f08..0da077dbc300 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart @@ -2,16 +2,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - -import 'package:flutter/rendering.dart'; +import 'package:flutter/foundation.dart'; import 'package:google_maps_flutter_platform_interface/src/types/maps_object.dart'; import 'package:google_maps_flutter_platform_interface/src/types/maps_object_updates.dart'; /// A trivial TestMapsObject implementation for testing updates with. -class TestMapsObject implements MapsObject { +@immutable +class TestMapsObject implements MapsObject { const TestMapsObject(this.mapsId, {this.data = 1}); + @override final MapsObjectId mapsId; final int data; @@ -37,7 +37,7 @@ class TestMapsObject implements MapsObject { } @override - int get hashCode => hashValues(mapsId, data); + int get hashCode => Object.hash(mapsId, data); } class TestMapsObjectUpdate extends MapsObjectUpdates { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart index 3a4c34764ef7..fe5d86335af3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart @@ -2,15 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; class _TestTileProvider extends TileProvider { @override Future getTile(int x, int y, int? zoom) async { - return Tile(0, 0, null); + return const Tile(0, 0, null); } } @@ -37,7 +35,6 @@ void main() { const TileOverlay tileOverlay = TileOverlay( tileOverlayId: TileOverlayId('id'), fadeIn: false, - tileProvider: null, transparency: 0.1, zIndex: 1, visible: false, @@ -67,7 +64,7 @@ void main() { test('equality', () async { final TileProvider tileProvider = _TestTileProvider(); final TileOverlay tileOverlay1 = TileOverlay( - tileOverlayId: TileOverlayId('id1'), + tileOverlayId: const TileOverlayId('id1'), fadeIn: false, tileProvider: tileProvider, transparency: 0.1, @@ -75,7 +72,7 @@ void main() { visible: false, tileSize: 128); final TileOverlay tileOverlaySameValues = TileOverlay( - tileOverlayId: TileOverlayId('id1'), + tileOverlayId: const TileOverlayId('id1'), fadeIn: false, tileProvider: tileProvider, transparency: 0.1, @@ -83,17 +80,16 @@ void main() { visible: false, tileSize: 128); final TileOverlay tileOverlayDifferentId = TileOverlay( - tileOverlayId: TileOverlayId('id2'), + tileOverlayId: const TileOverlayId('id2'), fadeIn: false, tileProvider: tileProvider, transparency: 0.1, zIndex: 1, visible: false, tileSize: 128); - final TileOverlay tileOverlayDifferentProvider = TileOverlay( + const TileOverlay tileOverlayDifferentProvider = TileOverlay( tileOverlayId: TileOverlayId('id1'), fadeIn: false, - tileProvider: null, transparency: 0.1, zIndex: 1, visible: false, @@ -107,7 +103,7 @@ void main() { final TileProvider tileProvider = _TestTileProvider(); // Set non-default values for every parameter. final TileOverlay tileOverlay = TileOverlay( - tileOverlayId: TileOverlayId('id1'), + tileOverlayId: const TileOverlayId('id1'), fadeIn: false, tileProvider: tileProvider, transparency: 0.1, @@ -130,7 +126,7 @@ void main() { tileSize: 128); expect( tileOverlay.hashCode, - hashValues( + Object.hash( tileOverlay.tileOverlayId, tileOverlay.fadeIn, tileOverlay.tileProvider, diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart index 05be14e1ba0b..b62f7326d831 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues, hashList; - import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter_platform_interface/src/types/tile_overlay.dart'; import 'package:google_maps_flutter_platform_interface/src/types/tile_overlay_updates.dart'; @@ -20,20 +18,20 @@ void main() { const TileOverlay to3Changed = TileOverlay(tileOverlayId: TileOverlayId('id3'), transparency: 0.5); const TileOverlay to4 = TileOverlay(tileOverlayId: TileOverlayId('id4')); - final Set previous = Set.from([to1, to2, to3]); - final Set current = - Set.from([to2, to3Changed, to4]); + final Set previous = {to1, to2, to3}; + final Set current = {to2, to3Changed, to4}; final TileOverlayUpdates updates = TileOverlayUpdates.from(previous, current); - final Set toRemove = - Set.from([const TileOverlayId('id1')]); + final Set toRemove = { + const TileOverlayId('id1') + }; expect(updates.tileOverlayIdsToRemove, toRemove); - final Set toAdd = Set.from([to4]); + final Set toAdd = {to4}; expect(updates.tileOverlaysToAdd, toAdd); - final Set toChange = Set.from([to3Changed]); + final Set toChange = {to3Changed}; expect(updates.tileOverlaysToChange, toChange); }); @@ -44,9 +42,8 @@ void main() { const TileOverlay to3Changed = TileOverlay(tileOverlayId: TileOverlayId('id3'), transparency: 0.5); const TileOverlay to4 = TileOverlay(tileOverlayId: TileOverlayId('id4')); - final Set previous = Set.from([to1, to2, to3]); - final Set current = - Set.from([to2, to3Changed, to4]); + final Set previous = {to1, to2, to3}; + final Set current = {to2, to3Changed, to4}; final TileOverlayUpdates updates = TileOverlayUpdates.from(previous, current); @@ -68,12 +65,10 @@ void main() { const TileOverlay to3Changed = TileOverlay(tileOverlayId: TileOverlayId('id3'), transparency: 0.5); const TileOverlay to4 = TileOverlay(tileOverlayId: TileOverlayId('id4')); - final Set previous = Set.from([to1, to2, to3]); - final Set current1 = - Set.from([to2, to3Changed, to4]); - final Set current2 = - Set.from([to2, to3Changed, to4]); - final Set current3 = Set.from([to2, to4]); + final Set previous = {to1, to2, to3}; + final Set current1 = {to2, to3Changed, to4}; + final Set current2 = {to2, to3Changed, to4}; + final Set current3 = {to2, to4}; final TileOverlayUpdates updates1 = TileOverlayUpdates.from(previous, current1); final TileOverlayUpdates updates2 = @@ -91,17 +86,16 @@ void main() { const TileOverlay to3Changed = TileOverlay(tileOverlayId: TileOverlayId('id3'), transparency: 0.5); const TileOverlay to4 = TileOverlay(tileOverlayId: TileOverlayId('id4')); - final Set previous = Set.from([to1, to2, to3]); - final Set current = - Set.from([to2, to3Changed, to4]); + final Set previous = {to1, to2, to3}; + final Set current = {to2, to3Changed, to4}; final TileOverlayUpdates updates = TileOverlayUpdates.from(previous, current); expect( updates.hashCode, - hashValues( - hashList(updates.tileOverlaysToAdd), - hashList(updates.tileOverlayIdsToRemove), - hashList(updates.tileOverlaysToChange))); + Object.hash( + Object.hashAll(updates.tileOverlaysToAdd), + Object.hashAll(updates.tileOverlayIdsToRemove), + Object.hashAll(updates.tileOverlaysToChange))); }); test('toString', () async { @@ -111,9 +105,8 @@ void main() { const TileOverlay to3Changed = TileOverlay(tileOverlayId: TileOverlayId('id3'), transparency: 0.5); const TileOverlay to4 = TileOverlay(tileOverlayId: TileOverlayId('id4')); - final Set previous = Set.from([to1, to2, to3]); - final Set current = - Set.from([to2, to3Changed, to4]); + final Set previous = {to1, to2, to3}; + final Set current = {to2, to3Changed, to4}; final TileOverlayUpdates updates = TileOverlayUpdates.from(previous, current); expect( diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.dart index 653958474185..ab49fd1a6c56 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.dart @@ -12,7 +12,7 @@ void main() { group('tile tests', () { test('toJson returns correct format', () async { - final Uint8List data = Uint8List.fromList([0, 1]); + final Uint8List data = Uint8List.fromList([0, 1]); final Tile tile = Tile(100, 200, data); final Object json = tile.toJson(); expect(json, { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/utils/map_configuration_serialization_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/utils/map_configuration_serialization_test.dart new file mode 100644 index 000000000000..71a0f8c4b1b1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/utils/map_configuration_serialization_test.dart @@ -0,0 +1,75 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/utils/map_configuration_serialization.dart'; + +void main() { + test('empty serialization', () async { + const MapConfiguration config = MapConfiguration(); + + final Map json = jsonForMapConfiguration(config); + + expect(json.isEmpty, true); + }); + + test('complete serialization', () async { + final MapConfiguration config = MapConfiguration( + compassEnabled: false, + mapToolbarEnabled: false, + cameraTargetBounds: CameraTargetBounds(LatLngBounds( + northeast: const LatLng(30, 20), southwest: const LatLng(10, 40))), + mapType: MapType.normal, + minMaxZoomPreference: const MinMaxZoomPreference(1.0, 10.0), + rotateGesturesEnabled: false, + scrollGesturesEnabled: false, + tiltGesturesEnabled: false, + trackCameraPosition: false, + zoomControlsEnabled: false, + zoomGesturesEnabled: false, + liteModeEnabled: false, + myLocationEnabled: false, + myLocationButtonEnabled: false, + padding: const EdgeInsets.all(5.0), + indoorViewEnabled: false, + trafficEnabled: false, + buildingsEnabled: false, + ); + + final Map json = jsonForMapConfiguration(config); + + // This uses literals instead of toJson() for the expectations on + // sub-objects, because if the serialization of any of those objects were + // ever to change MapConfiguration would need to update to serialize those + // objects manually to preserve the format, in order to avoid breaking + // implementations. + expect(json, { + 'compassEnabled': false, + 'mapToolbarEnabled': false, + 'cameraTargetBounds': [ + [ + [10.0, 40.0], + [30.0, 20.0] + ] + ], + 'mapType': 1, + 'minMaxZoomPreference': [1.0, 10.0], + 'rotateGesturesEnabled': false, + 'scrollGesturesEnabled': false, + 'tiltGesturesEnabled': false, + 'zoomControlsEnabled': false, + 'zoomGesturesEnabled': false, + 'liteModeEnabled': false, + 'trackCameraPosition': false, + 'myLocationEnabled': false, + 'myLocationButtonEnabled': false, + 'padding': [5.0, 5.0, 5.0, 5.0], + 'indoorEnabled': false, + 'trafficEnabled': false, + 'buildingsEnabled': false + }); + }); +} 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 8a3b94151de2..2333f7d16028 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,45 @@ +## NEXT + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. + +## 0.4.0+3 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. + +## 0.4.0+2 + +* Updates conversion of `BitmapDescriptor.fromBytes` marker icons to support the + new `size` parameter. Issue [#73789](https://github.com/flutter/flutter/issues/73789). +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 0.4.0+1 + +* Updates `README.md` to describe a hit-testing issue when Flutter widgets are overlaid on top of the Map widget. + +## 0.4.0 + +* Implements the new platform interface versions of `buildView` and + `updateOptions` with structured option types. +* **BREAKING CHANGE**: No longer implements the unstructured option dictionary + versions of those methods, so this version can only be used with + `google_maps_flutter` 2.1.8 or later. +* Adds `const` constructor parameters in example tests. + +## 0.3.3 + +* Removes custom `analysis_options.yaml` (and fixes code to comply with newest rules). +* Updates `package:google_maps` dependency to latest (`^6.1.0`). +* Ensures that `convert.dart` sanitizes user-created HTML before passing it to the + Maps JS SDK with `sanitizeHtml` from `package:sanitize_html`. + [More info](https://pub.dev/documentation/sanitize_html/latest/sanitize_html/sanitizeHtml.html). + +## 0.3.2+2 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 0.3.2+1 * Removes dependency on `meta`. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/README.md b/packages/google_maps_flutter/google_maps_flutter_web/README.md index 9e7ce94e3e59..692814731bec 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md @@ -46,3 +46,5 @@ There's no `defaultMarkerWithHue` in web. If you need colored pins/markers, you Indoor and building layers are still not available on the web. Traffic is. Only Android supports "[Lite Mode](https://developers.google.com/maps/documentation/android-sdk/lite)", so the `liteModeEnabled` constructor argument can't be set to `true` on web apps. + +Google Maps for web uses `HtmlElementView` to render maps. When a `GoogleMap` is stacked below other widgets, [`package:pointer_interceptor`](https://www.pub.dev/packages/pointer_interceptor) must be used to capture mouse events on the Flutter overlays. See issue [#73830](https://github.com/flutter/flutter/issues/73830). diff --git a/packages/google_maps_flutter/google_maps_flutter_web/analysis_options.yaml b/packages/google_maps_flutter/google_maps_flutter_web/analysis_options.yaml deleted file mode 100644 index 5aeb4e7c5e21..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../../analysis_options_legacy.yaml diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/README.md b/packages/google_maps_flutter/google_maps_flutter_web/example/README.md index 3cdecfab2ab9..0e51ae5ecbd2 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/README.md @@ -1,4 +1,14 @@ -# Testing +# 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. + +## Testing This package uses `package:integration_test` to run its tests in a web browser. @@ -7,6 +17,3 @@ in the Flutter wiki for instructions to setup and run the tests in this package. Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. - -See [Plugin Tests > Web Tests > Mocks](https://github.com/flutter/flutter/wiki/Plugin-Tests#mocks) -in the Flutter wiki for more information about the `.mocks.dart` files in this package. \ No newline at end of file diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart index 39aa641b10e4..0226234ea97a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart @@ -18,13 +18,13 @@ import 'google_maps_controller_test.mocks.dart'; // This value is used when comparing long~num, like // LatLng values. -const _acceptableDelta = 0.0000000001; +const double _acceptableDelta = 0.0000000001; -@GenerateMocks([], customMocks: [ - MockSpec(returnNullOnMissingStub: true), - MockSpec(returnNullOnMissingStub: true), - MockSpec(returnNullOnMissingStub: true), - MockSpec(returnNullOnMissingStub: true), +@GenerateMocks([], customMocks: >[ + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), ]) /// Test Google Map Controller @@ -32,51 +32,47 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('GoogleMapController', () { - final int mapId = 33930; + const int mapId = 33930; late GoogleMapController controller; - late StreamController stream; + late StreamController> stream; // Creates a controller with the default mapId and stream controller, and any `options` needed. - GoogleMapController _createController({ + GoogleMapController createController({ CameraPosition initialCameraPosition = const CameraPosition(target: LatLng(0, 0)), - Set markers = const {}, - Set polygons = const {}, - Set polylines = const {}, - Set circles = const {}, - Map options = const {}, + MapObjects mapObjects = const MapObjects(), + MapConfiguration mapConfiguration = const MapConfiguration(), }) { return GoogleMapController( mapId: mapId, streamController: stream, - initialCameraPosition: initialCameraPosition, - markers: markers, - polygons: polygons, - polylines: polylines, - circles: circles, - mapOptions: options, + widgetConfiguration: MapWidgetConfiguration( + initialCameraPosition: initialCameraPosition, + textDirection: TextDirection.ltr), + mapObjects: mapObjects, + mapConfiguration: mapConfiguration, ); } setUp(() { - stream = StreamController.broadcast(); + stream = StreamController>.broadcast(); }); group('construct/dispose', () { setUp(() { - controller = _createController(); + controller = createController(); }); testWidgets('constructor creates widget', (WidgetTester tester) async { expect(controller.widget, isNotNull); expect(controller.widget, isA()); - expect((controller.widget as HtmlElementView).viewType, + expect((controller.widget! as HtmlElementView).viewType, endsWith('$mapId')); }); testWidgets('widget is cached when reused', (WidgetTester tester) async { - final first = controller.widget; - final again = controller.widget; + final Widget? first = controller.widget; + final Widget? again = controller.widget; expect(identical(first, again), isTrue); }); @@ -104,7 +100,7 @@ void main() { expect(() async { await controller.getScreenCoordinate( - LatLng(43.3072465, -5.6918241), + const LatLng(43.3072465, -5.6918241), ); }, throwsAssertionError); }); @@ -115,7 +111,7 @@ void main() { expect(() async { await controller.getLatLng( - ScreenCoordinate(x: 640, y: 480), + const ScreenCoordinate(x: 640, y: 480), ); }, throwsAssertionError); }); @@ -143,7 +139,12 @@ void main() { controller.dispose(); expect(() { - controller.updateCircles(CircleUpdates.from({}, {})); + controller.updateCircles( + CircleUpdates.from( + const {}, + const {}, + ), + ); }, throwsAssertionError); }); @@ -152,7 +153,12 @@ void main() { controller.dispose(); expect(() { - controller.updatePolygons(PolygonUpdates.from({}, {})); + controller.updatePolygons( + PolygonUpdates.from( + const {}, + const {}, + ), + ); }, throwsAssertionError); }); @@ -161,7 +167,12 @@ void main() { controller.dispose(); expect(() { - controller.updatePolylines(PolylineUpdates.from({}, {})); + controller.updatePolylines( + PolylineUpdates.from( + const {}, + const {}, + ), + ); }, throwsAssertionError); }); @@ -170,15 +181,20 @@ void main() { controller.dispose(); expect(() { - controller.updateMarkers(MarkerUpdates.from({}, {})); + controller.updateMarkers( + MarkerUpdates.from( + const {}, + const {}, + ), + ); }, throwsAssertionError); expect(() { - controller.showInfoWindow(MarkerId('any')); + controller.showInfoWindow(const MarkerId('any')); }, throwsAssertionError); expect(() { - controller.hideInfoWindow(MarkerId('any')); + controller.hideInfoWindow(const MarkerId('any')); }, throwsAssertionError); }); @@ -186,7 +202,7 @@ void main() { (WidgetTester tester) async { controller.dispose(); - expect(controller.isInfoWindowShown(MarkerId('any')), false); + expect(controller.isInfoWindowShown(const MarkerId('any')), false); }); }); }); @@ -207,7 +223,7 @@ void main() { }); testWidgets('listens to map events', (WidgetTester tester) async { - controller = _createController(); + controller = createController(); controller.debugSetOverrides( createMap: (_, __) => map, circles: circles, @@ -219,16 +235,23 @@ void main() { controller.init(); // Trigger events on the map, and verify they've been broadcast to the stream - final capturedEvents = stream.stream.take(5); + final Stream> capturedEvents = stream.stream.take(5); gmaps.Event.trigger( - map, 'click', [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]); - gmaps.Event.trigger(map, 'rightclick', - [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]); - gmaps.Event.trigger(map, 'bounds_changed', []); // Causes 2 events - gmaps.Event.trigger(map, 'idle', []); + map, + 'click', + [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)], + ); + gmaps.Event.trigger( + map, + 'rightclick', + [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)], + ); + // The following line causes 2 events + gmaps.Event.trigger(map, 'bounds_changed', []); + gmaps.Event.trigger(map, 'idle', []); - final events = await capturedEvents.toList(); + final List> events = await capturedEvents.toList(); expect(events[0], isA()); expect(events[1], isA()); @@ -237,9 +260,9 @@ void main() { expect(events[4], isA()); }); - testWidgets('binds geometry controllers to map\'s', + testWidgets("binds geometry controllers to map's", (WidgetTester tester) async { - controller = _createController(); + controller = createController(); controller.debugSetOverrides( createMap: (_, __) => map, circles: circles, @@ -257,50 +280,51 @@ void main() { }); testWidgets('renders initial geometry', (WidgetTester tester) async { - controller = _createController(circles: { - Circle( + controller = createController( + mapObjects: MapObjects(circles: { + const Circle( circleId: CircleId('circle-1'), zIndex: 1234, ), - }, markers: { - Marker( + }, markers: { + const Marker( markerId: MarkerId('marker-1'), infoWindow: InfoWindow( title: 'title for test', snippet: 'snippet for test', ), ), - }, polygons: { - Polygon(polygonId: PolygonId('polygon-1'), points: [ + }, polygons: { + const Polygon(polygonId: PolygonId('polygon-1'), points: [ LatLng(43.355114, -5.851333), LatLng(43.354797, -5.851860), LatLng(43.354469, -5.851318), LatLng(43.354762, -5.850824), ]), - Polygon( + const Polygon( polygonId: PolygonId('polygon-2-with-holes'), - points: [ + points: [ LatLng(43.355114, -5.851333), LatLng(43.354797, -5.851860), LatLng(43.354469, -5.851318), LatLng(43.354762, -5.850824), ], - holes: [ - [ + holes: >[ + [ LatLng(41.354797, -6.851860), LatLng(41.354469, -6.851318), LatLng(41.354762, -6.850824), ] ], ), - }, polylines: { - Polyline(polylineId: PolylineId('polyline-1'), points: [ + }, polylines: { + const Polyline(polylineId: PolylineId('polyline-1'), points: [ LatLng(43.355114, -5.851333), LatLng(43.354797, -5.851860), LatLng(43.354469, -5.851318), LatLng(43.354762, -5.850824), ]) - }); + })); controller.debugSetOverrides( circles: circles, @@ -311,14 +335,16 @@ void main() { controller.init(); - final capturedCircles = + final Set capturedCircles = verify(circles.addCircles(captureAny)).captured[0] as Set; - final capturedMarkers = + final Set capturedMarkers = verify(markers.addMarkers(captureAny)).captured[0] as Set; - final capturedPolygons = verify(polygons.addPolygons(captureAny)) - .captured[0] as Set; - final capturedPolylines = verify(polylines.addPolylines(captureAny)) - .captured[0] as Set; + final Set capturedPolygons = + verify(polygons.addPolygons(captureAny)).captured[0] + as Set; + final Set capturedPolylines = + verify(polylines.addPolylines(captureAny)).captured[0] + as Set; expect(capturedCircles.first.circleId.value, 'circle-1'); expect(capturedCircles.first.zIndex, 1234); @@ -334,9 +360,10 @@ void main() { testWidgets('empty infoWindow does not create InfoWindow instance.', (WidgetTester tester) async { - controller = _createController(markers: { - Marker(markerId: MarkerId('marker-1')), - }); + controller = createController( + mapObjects: MapObjects(markers: { + const Marker(markerId: MarkerId('marker-1')), + })); controller.debugSetOverrides( markers: markers, @@ -344,7 +371,7 @@ void main() { controller.init(); - final capturedMarkers = + final Set capturedMarkers = verify(markers.addMarkers(captureAny)).captured[0] as Set; expect(capturedMarkers.first.infoWindow, InfoWindow.noText); @@ -356,11 +383,13 @@ void main() { capturedOptions = null; }); testWidgets('translates initial options', (WidgetTester tester) async { - controller = _createController(options: { - 'mapType': 2, - 'zoomControlsEnabled': true, - }); - controller.debugSetOverrides(createMap: (_, options) { + controller = createController( + mapConfiguration: const MapConfiguration( + mapType: MapType.satellite, + zoomControlsEnabled: true, + )); + controller.debugSetOverrides( + createMap: (_, gmaps.MapOptions options) { capturedOptions = options; return map; }); @@ -377,10 +406,12 @@ void main() { testWidgets('disables gestureHandling with scrollGesturesEnabled false', (WidgetTester tester) async { - controller = _createController(options: { - 'scrollGesturesEnabled': false, - }); - controller.debugSetOverrides(createMap: (_, options) { + controller = createController( + mapConfiguration: const MapConfiguration( + scrollGesturesEnabled: false, + )); + controller.debugSetOverrides( + createMap: (_, gmaps.MapOptions options) { capturedOptions = options; return map; }); @@ -395,10 +426,12 @@ void main() { testWidgets('disables gestureHandling with zoomGesturesEnabled false', (WidgetTester tester) async { - controller = _createController(options: { - 'zoomGesturesEnabled': false, - }); - controller.debugSetOverrides(createMap: (_, options) { + controller = createController( + mapConfiguration: const MapConfiguration( + zoomGesturesEnabled: false, + )); + controller.debugSetOverrides( + createMap: (_, gmaps.MapOptions options) { capturedOptions = options; return map; }); @@ -413,16 +446,15 @@ void main() { testWidgets('sets initial position when passed', (WidgetTester tester) async { - controller = _createController( - initialCameraPosition: CameraPosition( + controller = createController( + initialCameraPosition: const CameraPosition( target: LatLng(43.308, -5.6910), zoom: 12, - bearing: 0, - tilt: 0, ), ); - controller.debugSetOverrides(createMap: (_, options) { + controller.debugSetOverrides( + createMap: (_, gmaps.MapOptions options) { capturedOptions = options; return map; }); @@ -437,16 +469,17 @@ void main() { group('Traffic Layer', () { testWidgets('by default is disabled', (WidgetTester tester) async { - controller = _createController(); + controller = createController(); controller.init(); expect(controller.trafficLayer, isNull); }); testWidgets('initializes with traffic layer', (WidgetTester tester) async { - controller = _createController(options: { - 'trafficEnabled': true, - }); + controller = createController( + mapConfiguration: const MapConfiguration( + trafficEnabled: true, + )); controller.debugSetOverrides(createMap: (_, __) => map); controller.init(); expect(controller.trafficLayer, isNotNull); @@ -465,16 +498,16 @@ void main() { ..zoom = 10 ..center = gmaps.LatLng(0, 0), ); - controller = _createController(); + controller = createController(); controller.debugSetOverrides(createMap: (_, __) => map); controller.init(); }); group('updateRawOptions', () { testWidgets('can update `options`', (WidgetTester tester) async { - controller.updateRawOptions({ - 'mapType': 2, - }); + controller.updateMapConfiguration(const MapConfiguration( + mapType: MapType.satellite, + )); expect(map.mapTypeId, gmaps.MapTypeId.SATELLITE); }); @@ -482,15 +515,15 @@ void main() { testWidgets('can turn on/off traffic', (WidgetTester tester) async { expect(controller.trafficLayer, isNull); - controller.updateRawOptions({ - 'trafficEnabled': true, - }); + controller.updateMapConfiguration(const MapConfiguration( + trafficEnabled: true, + )); expect(controller.trafficLayer, isNotNull); - controller.updateRawOptions({ - 'trafficEnabled': false, - }); + controller.updateMapConfiguration(const MapConfiguration( + trafficEnabled: false, + )); expect(controller.trafficLayer, isNull); }); @@ -498,11 +531,11 @@ void main() { group('viewport getters', () { testWidgets('getVisibleRegion', (WidgetTester tester) async { - final gmCenter = map.center!; - final center = + final gmaps.LatLng gmCenter = map.center!; + final LatLng center = LatLng(gmCenter.lat.toDouble(), gmCenter.lng.toDouble()); - final bounds = await controller.getVisibleRegion(); + final LatLngBounds bounds = await controller.getVisibleRegion(); expect(bounds.contains(center), isTrue, reason: @@ -516,10 +549,14 @@ void main() { group('moveCamera', () { testWidgets('newLatLngZoom', (WidgetTester tester) async { - await (controller - .moveCamera(CameraUpdate.newLatLngZoom(LatLng(19, 26), 12))); + await controller.moveCamera( + CameraUpdate.newLatLngZoom( + const LatLng(19, 26), + 12, + ), + ); - final gmCenter = map.center!; + final gmaps.LatLng gmCenter = map.center!; expect(map.zoom, 12); expect(gmCenter.lat, closeTo(19, _acceptableDelta)); @@ -528,130 +565,133 @@ void main() { }); group('map.projection methods', () { - // These are too much for dart mockito, can't mock: - // map.projection.method() (in Javascript ;) ) - - // Caused https://github.com/flutter/flutter/issues/67606 + // Tested in projection_test.dart }); }); // These are the methods that get forwarded to other controllers, so we just verify calls. group('Pass-through methods', () { setUp(() { - controller = _createController(); + controller = createController(); }); testWidgets('updateCircles', (WidgetTester tester) async { - final mock = MockCirclesController(); + final MockCirclesController mock = MockCirclesController(); controller.debugSetOverrides(circles: mock); - final previous = { - Circle(circleId: CircleId('to-be-updated')), - Circle(circleId: CircleId('to-be-removed')), + final Set previous = { + const Circle(circleId: CircleId('to-be-updated')), + const Circle(circleId: CircleId('to-be-removed')), }; - final current = { - Circle(circleId: CircleId('to-be-updated'), visible: false), - Circle(circleId: CircleId('to-be-added')), + final Set current = { + const Circle(circleId: CircleId('to-be-updated'), visible: false), + const Circle(circleId: CircleId('to-be-added')), }; controller.updateCircles(CircleUpdates.from(previous, current)); - verify(mock.removeCircles({ - CircleId('to-be-removed'), + verify(mock.removeCircles({ + const CircleId('to-be-removed'), })); - verify(mock.addCircles({ - Circle(circleId: CircleId('to-be-added')), + verify(mock.addCircles({ + const Circle(circleId: CircleId('to-be-added')), })); - verify(mock.changeCircles({ - Circle(circleId: CircleId('to-be-updated'), visible: false), + verify(mock.changeCircles({ + const Circle(circleId: CircleId('to-be-updated'), visible: false), })); }); testWidgets('updateMarkers', (WidgetTester tester) async { - final mock = MockMarkersController(); + final MockMarkersController mock = MockMarkersController(); controller.debugSetOverrides(markers: mock); - final previous = { - Marker(markerId: MarkerId('to-be-updated')), - Marker(markerId: MarkerId('to-be-removed')), + final Set previous = { + const Marker(markerId: MarkerId('to-be-updated')), + const Marker(markerId: MarkerId('to-be-removed')), }; - final current = { - Marker(markerId: MarkerId('to-be-updated'), visible: false), - Marker(markerId: MarkerId('to-be-added')), + final Set current = { + const Marker(markerId: MarkerId('to-be-updated'), visible: false), + const Marker(markerId: MarkerId('to-be-added')), }; controller.updateMarkers(MarkerUpdates.from(previous, current)); - verify(mock.removeMarkers({ - MarkerId('to-be-removed'), + verify(mock.removeMarkers({ + const MarkerId('to-be-removed'), })); - verify(mock.addMarkers({ - Marker(markerId: MarkerId('to-be-added')), + verify(mock.addMarkers({ + const Marker(markerId: MarkerId('to-be-added')), })); - verify(mock.changeMarkers({ - Marker(markerId: MarkerId('to-be-updated'), visible: false), + verify(mock.changeMarkers({ + const Marker(markerId: MarkerId('to-be-updated'), visible: false), })); }); testWidgets('updatePolygons', (WidgetTester tester) async { - final mock = MockPolygonsController(); + final MockPolygonsController mock = MockPolygonsController(); controller.debugSetOverrides(polygons: mock); - final previous = { - Polygon(polygonId: PolygonId('to-be-updated')), - Polygon(polygonId: PolygonId('to-be-removed')), + final Set previous = { + const Polygon(polygonId: PolygonId('to-be-updated')), + const Polygon(polygonId: PolygonId('to-be-removed')), }; - final current = { - Polygon(polygonId: PolygonId('to-be-updated'), visible: false), - Polygon(polygonId: PolygonId('to-be-added')), + final Set current = { + const Polygon(polygonId: PolygonId('to-be-updated'), visible: false), + const Polygon(polygonId: PolygonId('to-be-added')), }; controller.updatePolygons(PolygonUpdates.from(previous, current)); - verify(mock.removePolygons({ - PolygonId('to-be-removed'), + verify(mock.removePolygons({ + const PolygonId('to-be-removed'), })); - verify(mock.addPolygons({ - Polygon(polygonId: PolygonId('to-be-added')), + verify(mock.addPolygons({ + const Polygon(polygonId: PolygonId('to-be-added')), })); - verify(mock.changePolygons({ - Polygon(polygonId: PolygonId('to-be-updated'), visible: false), + verify(mock.changePolygons({ + const Polygon(polygonId: PolygonId('to-be-updated'), visible: false), })); }); testWidgets('updatePolylines', (WidgetTester tester) async { - final mock = MockPolylinesController(); + final MockPolylinesController mock = MockPolylinesController(); controller.debugSetOverrides(polylines: mock); - final previous = { - Polyline(polylineId: PolylineId('to-be-updated')), - Polyline(polylineId: PolylineId('to-be-removed')), + final Set previous = { + const Polyline(polylineId: PolylineId('to-be-updated')), + const Polyline(polylineId: PolylineId('to-be-removed')), }; - final current = { - Polyline(polylineId: PolylineId('to-be-updated'), visible: false), - Polyline(polylineId: PolylineId('to-be-added')), + final Set current = { + const Polyline( + polylineId: PolylineId('to-be-updated'), + visible: false, + ), + const Polyline(polylineId: PolylineId('to-be-added')), }; controller.updatePolylines(PolylineUpdates.from(previous, current)); - verify(mock.removePolylines({ - PolylineId('to-be-removed'), + verify(mock.removePolylines({ + const PolylineId('to-be-removed'), })); - verify(mock.addPolylines({ - Polyline(polylineId: PolylineId('to-be-added')), + verify(mock.addPolylines({ + const Polyline(polylineId: PolylineId('to-be-added')), })); - verify(mock.changePolylines({ - Polyline(polylineId: PolylineId('to-be-updated'), visible: false), + verify(mock.changePolylines({ + const Polyline( + polylineId: PolylineId('to-be-updated'), + visible: false, + ), })); }); testWidgets('infoWindow visibility', (WidgetTester tester) async { - final mock = MockMarkersController(); - final markerId = MarkerId('marker-with-infowindow'); + final MockMarkersController mock = MockMarkersController(); + const MarkerId markerId = MarkerId('marker-with-infowindow'); when(mock.isInfoWindowShown(markerId)).thenReturn(true); controller.debugSetOverrides(markers: mock); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart index 530707c6c328..efde66459327 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart @@ -1,13 +1,15 @@ -// Mocks generated by Mockito 5.0.16 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_controller_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:google_maps/google_maps.dart' as _i2; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' as _i4; import 'package:google_maps_flutter_web/google_maps_flutter_web.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 @@ -16,50 +18,102 @@ import 'package:mockito/mockito.dart' as _i1; // 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 _FakeGMap_0 extends _i1.Fake implements _i2.GMap {} +class _FakeGMap_0 extends _i1.SmartFake implements _i2.GMap { + _FakeGMap_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} /// A class which mocks [CirclesController]. /// /// See the documentation for Mockito's code generation for more information. class MockCirclesController extends _i1.Mock implements _i3.CirclesController { @override - Map<_i4.CircleId, _i3.CircleController> get circles => - (super.noSuchMethod(Invocation.getter(#circles), - returnValue: <_i4.CircleId, _i3.CircleController>{}) - as Map<_i4.CircleId, _i3.CircleController>); - @override - _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), - returnValue: _FakeGMap_0()) as _i2.GMap); - @override - set googleMap(_i2.GMap? _googleMap) => - super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null); - @override - int get mapId => - (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int); - @override - set mapId(int? _mapId) => - super.noSuchMethod(Invocation.setter(#mapId, _mapId), - returnValueForMissingStub: null); - @override - void addCircles(Set<_i4.Circle>? circlesToAdd) => - super.noSuchMethod(Invocation.method(#addCircles, [circlesToAdd]), - returnValueForMissingStub: null); - @override - void changeCircles(Set<_i4.Circle>? circlesToChange) => - super.noSuchMethod(Invocation.method(#changeCircles, [circlesToChange]), - returnValueForMissingStub: null); + Map<_i4.CircleId, _i3.CircleController> get circles => (super.noSuchMethod( + Invocation.getter(#circles), + returnValue: <_i4.CircleId, _i3.CircleController>{}, + returnValueForMissingStub: <_i4.CircleId, _i3.CircleController>{}, + ) as Map<_i4.CircleId, _i3.CircleController>); + @override + _i2.GMap get googleMap => (super.noSuchMethod( + Invocation.getter(#googleMap), + returnValue: _FakeGMap_0( + this, + Invocation.getter(#googleMap), + ), + returnValueForMissingStub: _FakeGMap_0( + this, + Invocation.getter(#googleMap), + ), + ) as _i2.GMap); + @override + set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod( + Invocation.setter( + #googleMap, + _googleMap, + ), + returnValueForMissingStub: null, + ); + @override + int get mapId => (super.noSuchMethod( + Invocation.getter(#mapId), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + set mapId(int? _mapId) => super.noSuchMethod( + Invocation.setter( + #mapId, + _mapId, + ), + returnValueForMissingStub: null, + ); + @override + void addCircles(Set<_i4.Circle>? circlesToAdd) => super.noSuchMethod( + Invocation.method( + #addCircles, + [circlesToAdd], + ), + returnValueForMissingStub: null, + ); + @override + void changeCircles(Set<_i4.Circle>? circlesToChange) => super.noSuchMethod( + Invocation.method( + #changeCircles, + [circlesToChange], + ), + returnValueForMissingStub: null, + ); @override void removeCircles(Set<_i4.CircleId>? circleIdsToRemove) => - super.noSuchMethod(Invocation.method(#removeCircles, [circleIdsToRemove]), - returnValueForMissingStub: null); - @override - void bindToMap(int? mapId, _i2.GMap? googleMap) => - super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + super.noSuchMethod( + Invocation.method( + #removeCircles, + [circleIdsToRemove], + ), + returnValueForMissingStub: null, + ); + @override + void bindToMap( + int? mapId, + _i2.GMap? googleMap, + ) => + super.noSuchMethod( + Invocation.method( + #bindToMap, + [ + mapId, + googleMap, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [PolygonsController]. @@ -68,42 +122,85 @@ class MockCirclesController extends _i1.Mock implements _i3.CirclesController { class MockPolygonsController extends _i1.Mock implements _i3.PolygonsController { @override - Map<_i4.PolygonId, _i3.PolygonController> get polygons => - (super.noSuchMethod(Invocation.getter(#polygons), - returnValue: <_i4.PolygonId, _i3.PolygonController>{}) - as Map<_i4.PolygonId, _i3.PolygonController>); - @override - _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), - returnValue: _FakeGMap_0()) as _i2.GMap); - @override - set googleMap(_i2.GMap? _googleMap) => - super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null); - @override - int get mapId => - (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int); - @override - set mapId(int? _mapId) => - super.noSuchMethod(Invocation.setter(#mapId, _mapId), - returnValueForMissingStub: null); - @override - void addPolygons(Set<_i4.Polygon>? polygonsToAdd) => - super.noSuchMethod(Invocation.method(#addPolygons, [polygonsToAdd]), - returnValueForMissingStub: null); - @override - void changePolygons(Set<_i4.Polygon>? polygonsToChange) => - super.noSuchMethod(Invocation.method(#changePolygons, [polygonsToChange]), - returnValueForMissingStub: null); - @override - void removePolygons(Set<_i4.PolygonId>? polygonIdsToRemove) => super - .noSuchMethod(Invocation.method(#removePolygons, [polygonIdsToRemove]), - returnValueForMissingStub: null); - @override - void bindToMap(int? mapId, _i2.GMap? googleMap) => - super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + Map<_i4.PolygonId, _i3.PolygonController> get polygons => (super.noSuchMethod( + Invocation.getter(#polygons), + returnValue: <_i4.PolygonId, _i3.PolygonController>{}, + returnValueForMissingStub: <_i4.PolygonId, _i3.PolygonController>{}, + ) as Map<_i4.PolygonId, _i3.PolygonController>); + @override + _i2.GMap get googleMap => (super.noSuchMethod( + Invocation.getter(#googleMap), + returnValue: _FakeGMap_0( + this, + Invocation.getter(#googleMap), + ), + returnValueForMissingStub: _FakeGMap_0( + this, + Invocation.getter(#googleMap), + ), + ) as _i2.GMap); + @override + set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod( + Invocation.setter( + #googleMap, + _googleMap, + ), + returnValueForMissingStub: null, + ); + @override + int get mapId => (super.noSuchMethod( + Invocation.getter(#mapId), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + set mapId(int? _mapId) => super.noSuchMethod( + Invocation.setter( + #mapId, + _mapId, + ), + returnValueForMissingStub: null, + ); + @override + void addPolygons(Set<_i4.Polygon>? polygonsToAdd) => super.noSuchMethod( + Invocation.method( + #addPolygons, + [polygonsToAdd], + ), + returnValueForMissingStub: null, + ); + @override + void changePolygons(Set<_i4.Polygon>? polygonsToChange) => super.noSuchMethod( + Invocation.method( + #changePolygons, + [polygonsToChange], + ), + returnValueForMissingStub: null, + ); + @override + void removePolygons(Set<_i4.PolygonId>? polygonIdsToRemove) => + super.noSuchMethod( + Invocation.method( + #removePolygons, + [polygonIdsToRemove], + ), + returnValueForMissingStub: null, + ); + @override + void bindToMap( + int? mapId, + _i2.GMap? googleMap, + ) => + super.noSuchMethod( + Invocation.method( + #bindToMap, + [ + mapId, + googleMap, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [PolylinesController]. @@ -112,42 +209,86 @@ class MockPolygonsController extends _i1.Mock class MockPolylinesController extends _i1.Mock implements _i3.PolylinesController { @override - Map<_i4.PolylineId, _i3.PolylineController> get lines => - (super.noSuchMethod(Invocation.getter(#lines), - returnValue: <_i4.PolylineId, _i3.PolylineController>{}) - as Map<_i4.PolylineId, _i3.PolylineController>); - @override - _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), - returnValue: _FakeGMap_0()) as _i2.GMap); - @override - set googleMap(_i2.GMap? _googleMap) => - super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null); - @override - int get mapId => - (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int); - @override - set mapId(int? _mapId) => - super.noSuchMethod(Invocation.setter(#mapId, _mapId), - returnValueForMissingStub: null); - @override - void addPolylines(Set<_i4.Polyline>? polylinesToAdd) => - super.noSuchMethod(Invocation.method(#addPolylines, [polylinesToAdd]), - returnValueForMissingStub: null); - @override - void changePolylines(Set<_i4.Polyline>? polylinesToChange) => super - .noSuchMethod(Invocation.method(#changePolylines, [polylinesToChange]), - returnValueForMissingStub: null); - @override - void removePolylines(Set<_i4.PolylineId>? polylineIdsToRemove) => super - .noSuchMethod(Invocation.method(#removePolylines, [polylineIdsToRemove]), - returnValueForMissingStub: null); - @override - void bindToMap(int? mapId, _i2.GMap? googleMap) => - super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + Map<_i4.PolylineId, _i3.PolylineController> get lines => (super.noSuchMethod( + Invocation.getter(#lines), + returnValue: <_i4.PolylineId, _i3.PolylineController>{}, + returnValueForMissingStub: <_i4.PolylineId, _i3.PolylineController>{}, + ) as Map<_i4.PolylineId, _i3.PolylineController>); + @override + _i2.GMap get googleMap => (super.noSuchMethod( + Invocation.getter(#googleMap), + returnValue: _FakeGMap_0( + this, + Invocation.getter(#googleMap), + ), + returnValueForMissingStub: _FakeGMap_0( + this, + Invocation.getter(#googleMap), + ), + ) as _i2.GMap); + @override + set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod( + Invocation.setter( + #googleMap, + _googleMap, + ), + returnValueForMissingStub: null, + ); + @override + int get mapId => (super.noSuchMethod( + Invocation.getter(#mapId), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + set mapId(int? _mapId) => super.noSuchMethod( + Invocation.setter( + #mapId, + _mapId, + ), + returnValueForMissingStub: null, + ); + @override + void addPolylines(Set<_i4.Polyline>? polylinesToAdd) => super.noSuchMethod( + Invocation.method( + #addPolylines, + [polylinesToAdd], + ), + returnValueForMissingStub: null, + ); + @override + void changePolylines(Set<_i4.Polyline>? polylinesToChange) => + super.noSuchMethod( + Invocation.method( + #changePolylines, + [polylinesToChange], + ), + returnValueForMissingStub: null, + ); + @override + void removePolylines(Set<_i4.PolylineId>? polylineIdsToRemove) => + super.noSuchMethod( + Invocation.method( + #removePolylines, + [polylineIdsToRemove], + ), + returnValueForMissingStub: null, + ); + @override + void bindToMap( + int? mapId, + _i2.GMap? googleMap, + ) => + super.noSuchMethod( + Invocation.method( + #bindToMap, + [ + mapId, + googleMap, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [MarkersController]. @@ -155,52 +296,108 @@ class MockPolylinesController extends _i1.Mock /// See the documentation for Mockito's code generation for more information. class MockMarkersController extends _i1.Mock implements _i3.MarkersController { @override - Map<_i4.MarkerId, _i3.MarkerController> get markers => - (super.noSuchMethod(Invocation.getter(#markers), - returnValue: <_i4.MarkerId, _i3.MarkerController>{}) - as Map<_i4.MarkerId, _i3.MarkerController>); - @override - _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), - returnValue: _FakeGMap_0()) as _i2.GMap); - @override - set googleMap(_i2.GMap? _googleMap) => - super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), - returnValueForMissingStub: null); - @override - int get mapId => - (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int); - @override - set mapId(int? _mapId) => - super.noSuchMethod(Invocation.setter(#mapId, _mapId), - returnValueForMissingStub: null); - @override - void addMarkers(Set<_i4.Marker>? markersToAdd) => - super.noSuchMethod(Invocation.method(#addMarkers, [markersToAdd]), - returnValueForMissingStub: null); - @override - void changeMarkers(Set<_i4.Marker>? markersToChange) => - super.noSuchMethod(Invocation.method(#changeMarkers, [markersToChange]), - returnValueForMissingStub: null); + Map<_i4.MarkerId, _i3.MarkerController> get markers => (super.noSuchMethod( + Invocation.getter(#markers), + returnValue: <_i4.MarkerId, _i3.MarkerController>{}, + returnValueForMissingStub: <_i4.MarkerId, _i3.MarkerController>{}, + ) as Map<_i4.MarkerId, _i3.MarkerController>); + @override + _i2.GMap get googleMap => (super.noSuchMethod( + Invocation.getter(#googleMap), + returnValue: _FakeGMap_0( + this, + Invocation.getter(#googleMap), + ), + returnValueForMissingStub: _FakeGMap_0( + this, + Invocation.getter(#googleMap), + ), + ) as _i2.GMap); + @override + set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod( + Invocation.setter( + #googleMap, + _googleMap, + ), + returnValueForMissingStub: null, + ); + @override + int get mapId => (super.noSuchMethod( + Invocation.getter(#mapId), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + set mapId(int? _mapId) => super.noSuchMethod( + Invocation.setter( + #mapId, + _mapId, + ), + returnValueForMissingStub: null, + ); + @override + void addMarkers(Set<_i4.Marker>? markersToAdd) => super.noSuchMethod( + Invocation.method( + #addMarkers, + [markersToAdd], + ), + returnValueForMissingStub: null, + ); + @override + void changeMarkers(Set<_i4.Marker>? markersToChange) => super.noSuchMethod( + Invocation.method( + #changeMarkers, + [markersToChange], + ), + returnValueForMissingStub: null, + ); @override void removeMarkers(Set<_i4.MarkerId>? markerIdsToRemove) => - super.noSuchMethod(Invocation.method(#removeMarkers, [markerIdsToRemove]), - returnValueForMissingStub: null); - @override - void showMarkerInfoWindow(_i4.MarkerId? markerId) => - super.noSuchMethod(Invocation.method(#showMarkerInfoWindow, [markerId]), - returnValueForMissingStub: null); - @override - void hideMarkerInfoWindow(_i4.MarkerId? markerId) => - super.noSuchMethod(Invocation.method(#hideMarkerInfoWindow, [markerId]), - returnValueForMissingStub: null); - @override - bool isInfoWindowShown(_i4.MarkerId? markerId) => - (super.noSuchMethod(Invocation.method(#isInfoWindowShown, [markerId]), - returnValue: false) as bool); - @override - void bindToMap(int? mapId, _i2.GMap? googleMap) => - super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + super.noSuchMethod( + Invocation.method( + #removeMarkers, + [markerIdsToRemove], + ), + returnValueForMissingStub: null, + ); + @override + void showMarkerInfoWindow(_i4.MarkerId? markerId) => super.noSuchMethod( + Invocation.method( + #showMarkerInfoWindow, + [markerId], + ), + returnValueForMissingStub: null, + ); + @override + void hideMarkerInfoWindow(_i4.MarkerId? markerId) => super.noSuchMethod( + Invocation.method( + #hideMarkerInfoWindow, + [markerId], + ), + returnValueForMissingStub: null, + ); + @override + bool isInfoWindowShown(_i4.MarkerId? markerId) => (super.noSuchMethod( + Invocation.method( + #isInfoWindowShown, + [markerId], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + void bindToMap( + int? mapId, + _i2.GMap? googleMap, + ) => + super.noSuchMethod( + Invocation.method( + #bindToMap, + [ + mapId, + googleMap, + ], + ), + returnValueForMissingStub: null, + ); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart index a3cf86e593fe..9bd1a68c6207 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart @@ -5,20 +5,19 @@ import 'dart:async'; import 'dart:js_util' show getProperty; -import 'package:integration_test/integration_test.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; - import 'google_maps_plugin_test.mocks.dart'; -@GenerateMocks([], customMocks: [ - MockSpec(returnNullOnMissingStub: true), +@GenerateMocks([], customMocks: >[ + MockSpec(onMissingStub: OnMissingStub.returnDefault), ]) /// Test GoogleMapsPlugin @@ -51,7 +50,7 @@ void main() { group('after buildWidget', () { setUp(() { - plugin.debugSetMapById({0: controller}); + plugin.debugSetMapById({0: controller}); }); testWidgets('cannot call methods after dispose', @@ -69,19 +68,24 @@ void main() { }); group('buildView', () { - final testMapId = 33930; - final initialCameraPosition = CameraPosition(target: LatLng(0, 0)); + const int testMapId = 33930; + const CameraPosition initialCameraPosition = + CameraPosition(target: LatLng(0, 0)); testWidgets( 'returns an HtmlElementView and caches the controller for later', (WidgetTester tester) async { - final Map cache = {}; + final Map cache = + {}; plugin.debugSetMapById(cache); - final Widget widget = plugin.buildView( + final Widget widget = plugin.buildViewWithConfiguration( testMapId, onPlatformViewCreated, - initialCameraPosition: initialCameraPosition, + widgetConfiguration: const MapWidgetConfiguration( + initialCameraPosition: initialCameraPosition, + textDirection: TextDirection.ltr, + ), ); expect(widget, isA()); @@ -106,14 +110,20 @@ void main() { testWidgets('returns cached instance if it already exists', (WidgetTester tester) async { - final expected = HtmlElementView(viewType: 'only-for-testing'); + const HtmlElementView expected = + HtmlElementView(viewType: 'only-for-testing'); when(controller.widget).thenReturn(expected); - plugin.debugSetMapById({testMapId: controller}); + plugin.debugSetMapById({ + testMapId: controller, + }); - final widget = plugin.buildView( + final Widget widget = plugin.buildViewWithConfiguration( testMapId, onPlatformViewCreated, - initialCameraPosition: initialCameraPosition, + widgetConfiguration: const MapWidgetConfiguration( + initialCameraPosition: initialCameraPosition, + textDirection: TextDirection.ltr, + ), ); expect(widget, equals(expected)); @@ -122,13 +132,17 @@ void main() { testWidgets( 'asynchronously reports onPlatformViewCreated the first time it happens', (WidgetTester tester) async { - final Map cache = {}; + final Map cache = + {}; plugin.debugSetMapById(cache); - plugin.buildView( + plugin.buildViewWithConfiguration( testMapId, onPlatformViewCreated, - initialCameraPosition: initialCameraPosition, + widgetConfiguration: const MapWidgetConfiguration( + initialCameraPosition: initialCameraPosition, + textDirection: TextDirection.ltr, + ), ); // Simulate Google Maps JS SDK being "ready" @@ -157,47 +171,52 @@ void main() { }); group('setMapStyles', () { - String mapStyle = '''[{ - "featureType": "poi.park", - "elementType": "labels.text.fill", - "stylers": [{"color": "#6b9a76"}] - }]'''; + const String mapStyle = ''' +[{ + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [{"color": "#6b9a76"}] +}]'''; testWidgets('translates styles for controller', (WidgetTester tester) async { - plugin.debugSetMapById({0: controller}); + plugin.debugSetMapById({0: controller}); await plugin.setMapStyle(mapStyle, mapId: 0); - var captured = - verify(controller.updateRawOptions(captureThat(isMap))).captured[0]; + final dynamic captured = + verify(controller.updateStyles(captureThat(isList))).captured[0]; - expect(captured, contains('styles')); - var styles = captured['styles']; + final List styles = + captured as List; expect(styles.length, 1); // Let's peek inside the styles... - var style = styles[0] as gmaps.MapTypeStyle; + final gmaps.MapTypeStyle style = styles[0]; expect(style.featureType, 'poi.park'); expect(style.elementType, 'labels.text.fill'); expect(style.stylers?.length, 1); - expect(getProperty(style.stylers![0]!, 'color'), '#6b9a76'); + expect(getProperty(style.stylers![0]!, 'color'), '#6b9a76'); }); }); group('Noop methods:', () { - int mapId = 0; + const int mapId = 0; setUp(() { - plugin.debugSetMapById({mapId: controller}); + plugin.debugSetMapById({mapId: controller}); }); // Options testWidgets('updateTileOverlays', (WidgetTester tester) async { - final update = - plugin.updateTileOverlays(mapId: mapId, newTileOverlays: {}); + final Future update = plugin.updateTileOverlays( + mapId: mapId, + newTileOverlays: {}, + ); expect(update, completion(null)); }); testWidgets('updateTileOverlays', (WidgetTester tester) async { - final update = - plugin.clearTileCache(TileOverlayId('any'), mapId: mapId); + final Future update = plugin.clearTileCache( + const TileOverlayId('any'), + mapId: mapId, + ); expect(update, completion(null)); }); }); @@ -205,42 +224,55 @@ void main() { // These methods only pass-through values from the plugin to the controller // so we verify them all together here... group('Pass-through methods:', () { - int mapId = 0; + const int mapId = 0; setUp(() { - plugin.debugSetMapById({mapId: controller}); + plugin.debugSetMapById({mapId: controller}); }); // Options - testWidgets('updateMapOptions', (WidgetTester tester) async { - final expectedMapOptions = {'someOption': 12345}; + testWidgets('updateMapConfiguration', (WidgetTester tester) async { + const MapConfiguration configuration = + MapConfiguration(mapType: MapType.satellite); - await plugin.updateMapOptions(expectedMapOptions, mapId: mapId); + await plugin.updateMapConfiguration(configuration, mapId: mapId); - verify(controller.updateRawOptions(expectedMapOptions)); + verify(controller.updateMapConfiguration(configuration)); }); // Geometry testWidgets('updateMarkers', (WidgetTester tester) async { - final expectedUpdates = MarkerUpdates.from({}, {}); + final MarkerUpdates expectedUpdates = MarkerUpdates.from( + const {}, + const {}, + ); await plugin.updateMarkers(expectedUpdates, mapId: mapId); verify(controller.updateMarkers(expectedUpdates)); }); testWidgets('updatePolygons', (WidgetTester tester) async { - final expectedUpdates = PolygonUpdates.from({}, {}); + final PolygonUpdates expectedUpdates = PolygonUpdates.from( + const {}, + const {}, + ); await plugin.updatePolygons(expectedUpdates, mapId: mapId); verify(controller.updatePolygons(expectedUpdates)); }); testWidgets('updatePolylines', (WidgetTester tester) async { - final expectedUpdates = PolylineUpdates.from({}, {}); + final PolylineUpdates expectedUpdates = PolylineUpdates.from( + const {}, + const {}, + ); await plugin.updatePolylines(expectedUpdates, mapId: mapId); verify(controller.updatePolylines(expectedUpdates)); }); testWidgets('updateCircles', (WidgetTester tester) async { - final expectedUpdates = CircleUpdates.from({}, {}); + final CircleUpdates expectedUpdates = CircleUpdates.from( + const {}, + const {}, + ); await plugin.updateCircles(expectedUpdates, mapId: mapId); @@ -248,16 +280,18 @@ void main() { }); // Camera testWidgets('animateCamera', (WidgetTester tester) async { - final expectedUpdates = - CameraUpdate.newLatLng(LatLng(43.3626, -5.8433)); + final CameraUpdate expectedUpdates = CameraUpdate.newLatLng( + const LatLng(43.3626, -5.8433), + ); await plugin.animateCamera(expectedUpdates, mapId: mapId); verify(controller.moveCamera(expectedUpdates)); }); testWidgets('moveCamera', (WidgetTester tester) async { - final expectedUpdates = - CameraUpdate.newLatLng(LatLng(43.3628, -5.8478)); + final CameraUpdate expectedUpdates = CameraUpdate.newLatLng( + const LatLng(43.3628, -5.8478), + ); await plugin.moveCamera(expectedUpdates, mapId: mapId); @@ -268,8 +302,8 @@ void main() { testWidgets('getVisibleRegion', (WidgetTester tester) async { when(controller.getVisibleRegion()) .thenAnswer((_) async => LatLngBounds( - northeast: LatLng(47.2359634, -68.0192019), - southwest: LatLng(34.5019594, -120.4974629), + northeast: const LatLng(47.2359634, -68.0192019), + southwest: const LatLng(34.5019594, -120.4974629), )); await plugin.getVisibleRegion(mapId: mapId); @@ -285,10 +319,10 @@ void main() { testWidgets('getScreenCoordinate', (WidgetTester tester) async { when(controller.getScreenCoordinate(any)).thenAnswer( - (_) async => ScreenCoordinate(x: 320, y: 240) // fake return + (_) async => const ScreenCoordinate(x: 320, y: 240) // fake return ); - final latLng = LatLng(43.3613, -5.8499); + const LatLng latLng = LatLng(43.3613, -5.8499); await plugin.getScreenCoordinate(latLng, mapId: mapId); @@ -296,11 +330,11 @@ void main() { }); testWidgets('getLatLng', (WidgetTester tester) async { - when(controller.getLatLng(any)) - .thenAnswer((_) async => LatLng(43.3613, -5.8499) // fake return - ); + when(controller.getLatLng(any)).thenAnswer( + (_) async => const LatLng(43.3613, -5.8499) // fake return + ); - final coordinates = ScreenCoordinate(x: 19, y: 26); + const ScreenCoordinate coordinates = ScreenCoordinate(x: 19, y: 26); await plugin.getLatLng(coordinates, mapId: mapId); @@ -309,7 +343,7 @@ void main() { // InfoWindows testWidgets('showMarkerInfoWindow', (WidgetTester tester) async { - final markerId = MarkerId('testing-123'); + const MarkerId markerId = MarkerId('testing-123'); await plugin.showMarkerInfoWindow(markerId, mapId: mapId); @@ -317,7 +351,7 @@ void main() { }); testWidgets('hideMarkerInfoWindow', (WidgetTester tester) async { - final markerId = MarkerId('testing-123'); + const MarkerId markerId = MarkerId('testing-123'); await plugin.hideMarkerInfoWindow(markerId, mapId: mapId); @@ -327,7 +361,7 @@ void main() { testWidgets('isMarkerInfoWindowShown', (WidgetTester tester) async { when(controller.isInfoWindowShown(any)).thenReturn(true); - final markerId = MarkerId('testing-123'); + const MarkerId markerId = MarkerId('testing-123'); await plugin.isMarkerInfoWindowShown(markerId, mapId: mapId); @@ -337,18 +371,18 @@ void main() { // Verify all event streams are filtered correctly from the main one... group('Event Streams', () { - int mapId = 0; - late StreamController streamController; + const int mapId = 0; + late StreamController> streamController; setUp(() { - streamController = StreamController.broadcast(); + streamController = StreamController>.broadcast(); when(controller.events) - .thenAnswer((realInvocation) => streamController.stream); - plugin.debugSetMapById({mapId: controller}); + .thenAnswer((Invocation realInvocation) => streamController.stream); + plugin.debugSetMapById({mapId: controller}); }); // Dispatches a few events in the global streamController, and expects *only* the passed event to be there. - Future _testStreamFiltering( - Stream stream, MapEvent event) async { + Future testStreamFiltering( + Stream> stream, MapEvent event) async { Timer.run(() { streamController.add(_OtherMapEvent(mapId)); streamController.add(event); @@ -356,7 +390,7 @@ void main() { streamController.close(); }); - final events = await stream.toList(); + final List> events = await stream.toList(); expect(events.length, 1); expect(events[0], event); @@ -364,120 +398,151 @@ void main() { // Camera events testWidgets('onCameraMoveStarted', (WidgetTester tester) async { - final event = CameraMoveStartedEvent(mapId); + final CameraMoveStartedEvent event = CameraMoveStartedEvent(mapId); - final stream = plugin.onCameraMoveStarted(mapId: mapId); + final Stream stream = + plugin.onCameraMoveStarted(mapId: mapId); - await _testStreamFiltering(stream, event); + await testStreamFiltering(stream, event); }); testWidgets('onCameraMoveStarted', (WidgetTester tester) async { - final event = CameraMoveEvent( + final CameraMoveEvent event = CameraMoveEvent( mapId, - CameraPosition( + const CameraPosition( target: LatLng(43.3790, -5.8660), ), ); - final stream = plugin.onCameraMove(mapId: mapId); + final Stream stream = + plugin.onCameraMove(mapId: mapId); - await _testStreamFiltering(stream, event); + await testStreamFiltering(stream, event); }); testWidgets('onCameraIdle', (WidgetTester tester) async { - final event = CameraIdleEvent(mapId); + final CameraIdleEvent event = CameraIdleEvent(mapId); - final stream = plugin.onCameraIdle(mapId: mapId); + final Stream stream = + plugin.onCameraIdle(mapId: mapId); - await _testStreamFiltering(stream, event); + await testStreamFiltering(stream, event); }); // Marker events testWidgets('onMarkerTap', (WidgetTester tester) async { - final event = MarkerTapEvent(mapId, MarkerId('test-123')); + final MarkerTapEvent event = MarkerTapEvent( + mapId, + const MarkerId('test-123'), + ); - final stream = plugin.onMarkerTap(mapId: mapId); + final Stream stream = plugin.onMarkerTap(mapId: mapId); - await _testStreamFiltering(stream, event); + await testStreamFiltering(stream, event); }); testWidgets('onInfoWindowTap', (WidgetTester tester) async { - final event = InfoWindowTapEvent(mapId, MarkerId('test-123')); + final InfoWindowTapEvent event = InfoWindowTapEvent( + mapId, + const MarkerId('test-123'), + ); - final stream = plugin.onInfoWindowTap(mapId: mapId); + final Stream stream = + plugin.onInfoWindowTap(mapId: mapId); - await _testStreamFiltering(stream, event); + await testStreamFiltering(stream, event); }); testWidgets('onMarkerDragStart', (WidgetTester tester) async { - final event = MarkerDragStartEvent( + final MarkerDragStartEvent event = MarkerDragStartEvent( mapId, - LatLng(43.3677, -5.8372), - MarkerId('test-123'), + const LatLng(43.3677, -5.8372), + const MarkerId('test-123'), ); - final stream = plugin.onMarkerDragStart(mapId: mapId); + final Stream stream = + plugin.onMarkerDragStart(mapId: mapId); - await _testStreamFiltering(stream, event); + await testStreamFiltering(stream, event); }); testWidgets('onMarkerDrag', (WidgetTester tester) async { - final event = MarkerDragEvent( + final MarkerDragEvent event = MarkerDragEvent( mapId, - LatLng(43.3677, -5.8372), - MarkerId('test-123'), + const LatLng(43.3677, -5.8372), + const MarkerId('test-123'), ); - final stream = plugin.onMarkerDrag(mapId: mapId); + final Stream stream = + plugin.onMarkerDrag(mapId: mapId); - await _testStreamFiltering(stream, event); + await testStreamFiltering(stream, event); }); testWidgets('onMarkerDragEnd', (WidgetTester tester) async { - final event = MarkerDragEndEvent( + final MarkerDragEndEvent event = MarkerDragEndEvent( mapId, - LatLng(43.3677, -5.8372), - MarkerId('test-123'), + const LatLng(43.3677, -5.8372), + const MarkerId('test-123'), ); - final stream = plugin.onMarkerDragEnd(mapId: mapId); + final Stream stream = + plugin.onMarkerDragEnd(mapId: mapId); - await _testStreamFiltering(stream, event); + await testStreamFiltering(stream, event); }); // Geometry testWidgets('onPolygonTap', (WidgetTester tester) async { - final event = PolygonTapEvent(mapId, PolygonId('test-123')); + final PolygonTapEvent event = PolygonTapEvent( + mapId, + const PolygonId('test-123'), + ); - final stream = plugin.onPolygonTap(mapId: mapId); + final Stream stream = + plugin.onPolygonTap(mapId: mapId); - await _testStreamFiltering(stream, event); + await testStreamFiltering(stream, event); }); testWidgets('onPolylineTap', (WidgetTester tester) async { - final event = PolylineTapEvent(mapId, PolylineId('test-123')); + final PolylineTapEvent event = PolylineTapEvent( + mapId, + const PolylineId('test-123'), + ); - final stream = plugin.onPolylineTap(mapId: mapId); + final Stream stream = + plugin.onPolylineTap(mapId: mapId); - await _testStreamFiltering(stream, event); + await testStreamFiltering(stream, event); }); testWidgets('onCircleTap', (WidgetTester tester) async { - final event = CircleTapEvent(mapId, CircleId('test-123')); + final CircleTapEvent event = CircleTapEvent( + mapId, + const CircleId('test-123'), + ); - final stream = plugin.onCircleTap(mapId: mapId); + final Stream stream = plugin.onCircleTap(mapId: mapId); - await _testStreamFiltering(stream, event); + await testStreamFiltering(stream, event); }); // Map taps testWidgets('onTap', (WidgetTester tester) async { - final event = MapTapEvent(mapId, LatLng(43.3597, -5.8458)); + final MapTapEvent event = MapTapEvent( + mapId, + const LatLng(43.3597, -5.8458), + ); - final stream = plugin.onTap(mapId: mapId); + final Stream stream = plugin.onTap(mapId: mapId); - await _testStreamFiltering(stream, event); + await testStreamFiltering(stream, event); }); testWidgets('onLongPress', (WidgetTester tester) async { - final event = MapLongPressEvent(mapId, LatLng(43.3608, -5.8425)); + final MapLongPressEvent event = MapLongPressEvent( + mapId, + const LatLng(43.3608, -5.8425), + ); - final stream = plugin.onLongPress(mapId: mapId); + final Stream stream = + plugin.onLongPress(mapId: mapId); - await _testStreamFiltering(stream, event); + await testStreamFiltering(stream, event); }); }); }); } -class _OtherMapEvent extends MapEvent { +class _OtherMapEvent extends MapEvent { _OtherMapEvent(int mapId) : super(mapId, null); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart index d2df11c6ffa9..a85bce31e20f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart @@ -1,14 +1,17 @@ -// Mocks generated by Mockito 5.0.16 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_plugin_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i2; +import 'package:google_maps/google_maps.dart' as _i5; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' as _i3; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i4; 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 @@ -17,16 +20,49 @@ import 'package:mockito/mockito.dart' as _i1; // 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 _FakeStreamController_0 extends _i1.Fake - implements _i2.StreamController {} +class _FakeStreamController_0 extends _i1.SmartFake + implements _i2.StreamController { + _FakeStreamController_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeLatLngBounds_1 extends _i1.Fake implements _i3.LatLngBounds {} +class _FakeLatLngBounds_1 extends _i1.SmartFake implements _i3.LatLngBounds { + _FakeLatLngBounds_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeScreenCoordinate_2 extends _i1.Fake implements _i3.ScreenCoordinate { +class _FakeScreenCoordinate_2 extends _i1.SmartFake + implements _i3.ScreenCoordinate { + _FakeScreenCoordinate_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } -class _FakeLatLng_3 extends _i1.Fake implements _i3.LatLng {} +class _FakeLatLng_3 extends _i1.SmartFake implements _i3.LatLng { + _FakeLatLng_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} /// A class which mocks [GoogleMapController]. /// @@ -34,98 +70,227 @@ class _FakeLatLng_3 extends _i1.Fake implements _i3.LatLng {} class MockGoogleMapController extends _i1.Mock implements _i4.GoogleMapController { @override - _i2.StreamController<_i3.MapEvent> get stream => - (super.noSuchMethod(Invocation.getter(#stream), - returnValue: _FakeStreamController_0<_i3.MapEvent>()) - as _i2.StreamController<_i3.MapEvent>); - @override - _i2.Stream<_i3.MapEvent> get events => - (super.noSuchMethod(Invocation.getter(#events), - returnValue: Stream<_i3.MapEvent>.empty()) - as _i2.Stream<_i3.MapEvent>); - @override - bool get isInitialized => - (super.noSuchMethod(Invocation.getter(#isInitialized), returnValue: false) - as bool); - @override - void debugSetOverrides( - {_i4.DebugCreateMapFunction? createMap, - _i4.MarkersController? markers, - _i4.CirclesController? circles, - _i4.PolygonsController? polygons, - _i4.PolylinesController? polylines}) => + _i2.StreamController<_i3.MapEvent> get stream => (super.noSuchMethod( + Invocation.getter(#stream), + returnValue: _FakeStreamController_0<_i3.MapEvent>( + this, + Invocation.getter(#stream), + ), + returnValueForMissingStub: + _FakeStreamController_0<_i3.MapEvent>( + this, + Invocation.getter(#stream), + ), + ) as _i2.StreamController<_i3.MapEvent>); + @override + _i2.Stream<_i3.MapEvent> get events => (super.noSuchMethod( + Invocation.getter(#events), + returnValue: _i2.Stream<_i3.MapEvent>.empty(), + returnValueForMissingStub: _i2.Stream<_i3.MapEvent>.empty(), + ) as _i2.Stream<_i3.MapEvent>); + @override + bool get isInitialized => (super.noSuchMethod( + Invocation.getter(#isInitialized), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + void debugSetOverrides({ + _i4.DebugCreateMapFunction? createMap, + _i4.MarkersController? markers, + _i4.CirclesController? circles, + _i4.PolygonsController? polygons, + _i4.PolylinesController? polylines, + }) => super.noSuchMethod( - Invocation.method(#debugSetOverrides, [], { + Invocation.method( + #debugSetOverrides, + [], + { #createMap: createMap, #markers: markers, #circles: circles, #polygons: polygons, - #polylines: polylines - }), - returnValueForMissingStub: null); + #polylines: polylines, + }, + ), + returnValueForMissingStub: null, + ); + @override + void init() => super.noSuchMethod( + Invocation.method( + #init, + [], + ), + returnValueForMissingStub: null, + ); @override - void init() => super.noSuchMethod(Invocation.method(#init, []), - returnValueForMissingStub: null); + void updateMapConfiguration(_i3.MapConfiguration? update) => + super.noSuchMethod( + Invocation.method( + #updateMapConfiguration, + [update], + ), + returnValueForMissingStub: null, + ); @override - void updateRawOptions(Map? optionsUpdate) => - super.noSuchMethod(Invocation.method(#updateRawOptions, [optionsUpdate]), - returnValueForMissingStub: null); + void updateStyles(List<_i5.MapTypeStyle>? styles) => super.noSuchMethod( + Invocation.method( + #updateStyles, + [styles], + ), + returnValueForMissingStub: null, + ); @override _i2.Future<_i3.LatLngBounds> getVisibleRegion() => (super.noSuchMethod( - Invocation.method(#getVisibleRegion, []), - returnValue: Future<_i3.LatLngBounds>.value(_FakeLatLngBounds_1())) - as _i2.Future<_i3.LatLngBounds>); + Invocation.method( + #getVisibleRegion, + [], + ), + returnValue: _i2.Future<_i3.LatLngBounds>.value(_FakeLatLngBounds_1( + this, + Invocation.method( + #getVisibleRegion, + [], + ), + )), + returnValueForMissingStub: + _i2.Future<_i3.LatLngBounds>.value(_FakeLatLngBounds_1( + this, + Invocation.method( + #getVisibleRegion, + [], + ), + )), + ) as _i2.Future<_i3.LatLngBounds>); @override _i2.Future<_i3.ScreenCoordinate> getScreenCoordinate(_i3.LatLng? latLng) => - (super.noSuchMethod(Invocation.method(#getScreenCoordinate, [latLng]), - returnValue: - Future<_i3.ScreenCoordinate>.value(_FakeScreenCoordinate_2())) - as _i2.Future<_i3.ScreenCoordinate>); + (super.noSuchMethod( + Invocation.method( + #getScreenCoordinate, + [latLng], + ), + returnValue: + _i2.Future<_i3.ScreenCoordinate>.value(_FakeScreenCoordinate_2( + this, + Invocation.method( + #getScreenCoordinate, + [latLng], + ), + )), + returnValueForMissingStub: + _i2.Future<_i3.ScreenCoordinate>.value(_FakeScreenCoordinate_2( + this, + Invocation.method( + #getScreenCoordinate, + [latLng], + ), + )), + ) as _i2.Future<_i3.ScreenCoordinate>); @override _i2.Future<_i3.LatLng> getLatLng(_i3.ScreenCoordinate? screenCoordinate) => - (super.noSuchMethod(Invocation.method(#getLatLng, [screenCoordinate]), - returnValue: Future<_i3.LatLng>.value(_FakeLatLng_3())) - as _i2.Future<_i3.LatLng>); + (super.noSuchMethod( + Invocation.method( + #getLatLng, + [screenCoordinate], + ), + returnValue: _i2.Future<_i3.LatLng>.value(_FakeLatLng_3( + this, + Invocation.method( + #getLatLng, + [screenCoordinate], + ), + )), + returnValueForMissingStub: _i2.Future<_i3.LatLng>.value(_FakeLatLng_3( + this, + Invocation.method( + #getLatLng, + [screenCoordinate], + ), + )), + ) as _i2.Future<_i3.LatLng>); @override _i2.Future moveCamera(_i3.CameraUpdate? cameraUpdate) => - (super.noSuchMethod(Invocation.method(#moveCamera, [cameraUpdate]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i2.Future); - @override - _i2.Future getZoomLevel() => - (super.noSuchMethod(Invocation.method(#getZoomLevel, []), - returnValue: Future.value(0.0)) as _i2.Future); + (super.noSuchMethod( + Invocation.method( + #moveCamera, + [cameraUpdate], + ), + returnValue: _i2.Future.value(), + returnValueForMissingStub: _i2.Future.value(), + ) as _i2.Future); @override - void updateCircles(_i3.CircleUpdates? updates) => - super.noSuchMethod(Invocation.method(#updateCircles, [updates]), - returnValueForMissingStub: null); + _i2.Future getZoomLevel() => (super.noSuchMethod( + Invocation.method( + #getZoomLevel, + [], + ), + returnValue: _i2.Future.value(0.0), + returnValueForMissingStub: _i2.Future.value(0.0), + ) as _i2.Future); @override - void updatePolygons(_i3.PolygonUpdates? updates) => - super.noSuchMethod(Invocation.method(#updatePolygons, [updates]), - returnValueForMissingStub: null); + void updateCircles(_i3.CircleUpdates? updates) => super.noSuchMethod( + Invocation.method( + #updateCircles, + [updates], + ), + returnValueForMissingStub: null, + ); @override - void updatePolylines(_i3.PolylineUpdates? updates) => - super.noSuchMethod(Invocation.method(#updatePolylines, [updates]), - returnValueForMissingStub: null); + void updatePolygons(_i3.PolygonUpdates? updates) => super.noSuchMethod( + Invocation.method( + #updatePolygons, + [updates], + ), + returnValueForMissingStub: null, + ); @override - void updateMarkers(_i3.MarkerUpdates? updates) => - super.noSuchMethod(Invocation.method(#updateMarkers, [updates]), - returnValueForMissingStub: null); + void updatePolylines(_i3.PolylineUpdates? updates) => super.noSuchMethod( + Invocation.method( + #updatePolylines, + [updates], + ), + returnValueForMissingStub: null, + ); @override - void showInfoWindow(_i3.MarkerId? markerId) => - super.noSuchMethod(Invocation.method(#showInfoWindow, [markerId]), - returnValueForMissingStub: null); + void updateMarkers(_i3.MarkerUpdates? updates) => super.noSuchMethod( + Invocation.method( + #updateMarkers, + [updates], + ), + returnValueForMissingStub: null, + ); @override - void hideInfoWindow(_i3.MarkerId? markerId) => - super.noSuchMethod(Invocation.method(#hideInfoWindow, [markerId]), - returnValueForMissingStub: null); + void showInfoWindow(_i3.MarkerId? markerId) => super.noSuchMethod( + Invocation.method( + #showInfoWindow, + [markerId], + ), + returnValueForMissingStub: null, + ); @override - bool isInfoWindowShown(_i3.MarkerId? markerId) => - (super.noSuchMethod(Invocation.method(#isInfoWindowShown, [markerId]), - returnValue: false) as bool); + void hideInfoWindow(_i3.MarkerId? markerId) => super.noSuchMethod( + Invocation.method( + #hideInfoWindow, + [markerId], + ), + returnValueForMissingStub: null, + ); @override - void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), - returnValueForMissingStub: null); + bool isInfoWindowShown(_i3.MarkerId? markerId) => (super.noSuchMethod( + Invocation.method( + #isInfoWindowShown, + [markerId], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); @override - String toString() => super.toString(); + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart index cfa36febbbfe..6591b0ca08d7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart @@ -5,10 +5,10 @@ import 'dart:async'; import 'dart:html' as html; -import 'package:integration_test/integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps/google_maps.dart' as gmaps; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; /// Test Markers void main() { @@ -16,32 +16,32 @@ void main() { // Since onTap/DragEnd events happen asynchronously, we need to store when the event // is fired. We use a completer so the test can wait for the future to be completed. - late Completer _methodCalledCompleter; + late Completer methodCalledCompleter; - /// This is the future value of the [_methodCalledCompleter]. Reinitialized + /// This is the future value of the [methodCalledCompleter]. Reinitialized /// in the [setUp] method, and completed (as `true`) by [onTap] and [onDragEnd] /// when those methods are called from the MarkerController. late Future methodCalled; void onTap() { - _methodCalledCompleter.complete(true); + methodCalledCompleter.complete(true); } void onDragStart(gmaps.LatLng _) { - _methodCalledCompleter.complete(true); + methodCalledCompleter.complete(true); } void onDrag(gmaps.LatLng _) { - _methodCalledCompleter.complete(true); + methodCalledCompleter.complete(true); } void onDragEnd(gmaps.LatLng _) { - _methodCalledCompleter.complete(true); + methodCalledCompleter.complete(true); } setUp(() { - _methodCalledCompleter = Completer(); - methodCalled = _methodCalledCompleter.future; + methodCalledCompleter = Completer(); + methodCalled = methodCalledCompleter.future; }); group('MarkerController', () { @@ -55,7 +55,7 @@ void main() { MarkerController(marker: marker, onTap: onTap); // Trigger a click event... - gmaps.Event.trigger(marker, 'click', [gmaps.MapMouseEvent()]); + gmaps.Event.trigger(marker, 'click', [gmaps.MapMouseEvent()]); // The event handling is now truly async. Wait for it... expect(await methodCalled, isTrue); @@ -66,7 +66,7 @@ void main() { // Trigger a drag end event... gmaps.Event.trigger(marker, 'dragstart', - [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]); + [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]); expect(await methodCalled, isTrue); }); @@ -76,7 +76,10 @@ void main() { // Trigger a drag end event... gmaps.Event.trigger( - marker, 'drag', [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]); + marker, + 'drag', + [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)], + ); expect(await methodCalled, isTrue); }); @@ -85,15 +88,19 @@ void main() { MarkerController(marker: marker, onDragEnd: onDragEnd); // Trigger a drag end event... - gmaps.Event.trigger(marker, 'dragend', - [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]); + gmaps.Event.trigger( + marker, + 'dragend', + [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)], + ); expect(await methodCalled, isTrue); }); testWidgets('update', (WidgetTester tester) async { - final controller = MarkerController(marker: marker); - final options = gmaps.MarkerOptions()..draggable = true; + final MarkerController controller = MarkerController(marker: marker); + final gmaps.MarkerOptions options = gmaps.MarkerOptions() + ..draggable = true; expect(marker.draggable, isNull); @@ -104,7 +111,7 @@ void main() { testWidgets('infoWindow null, showInfoWindow.', (WidgetTester tester) async { - final controller = MarkerController(marker: marker); + final MarkerController controller = MarkerController(marker: marker); controller.showInfoWindow(); @@ -112,11 +119,13 @@ void main() { }); testWidgets('showInfoWindow', (WidgetTester tester) async { - final infoWindow = gmaps.InfoWindow(); - final map = gmaps.GMap(html.DivElement()); + final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); + final gmaps.GMap map = gmaps.GMap(html.DivElement()); marker.set('map', map); - final controller = - MarkerController(marker: marker, infoWindow: infoWindow); + final MarkerController controller = MarkerController( + marker: marker, + infoWindow: infoWindow, + ); controller.showInfoWindow(); @@ -125,11 +134,13 @@ void main() { }); testWidgets('hideInfoWindow', (WidgetTester tester) async { - final infoWindow = gmaps.InfoWindow(); - final map = gmaps.GMap(html.DivElement()); + final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); + final gmaps.GMap map = gmaps.GMap(html.DivElement()); marker.set('map', map); - final controller = - MarkerController(marker: marker, infoWindow: infoWindow); + final MarkerController controller = MarkerController( + marker: marker, + infoWindow: infoWindow, + ); controller.hideInfoWindow(); @@ -141,8 +152,8 @@ void main() { late MarkerController controller; setUp(() { - final infoWindow = gmaps.InfoWindow(); - final map = gmaps.GMap(html.DivElement()); + final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); + final gmaps.GMap map = gmaps.GMap(html.DivElement()); marker.set('map', map); controller = MarkerController(marker: marker, infoWindow: infoWindow); }); @@ -155,7 +166,8 @@ void main() { testWidgets('cannot call update after remove', (WidgetTester tester) async { - final options = gmaps.MarkerOptions()..draggable = true; + final gmaps.MarkerOptions options = gmaps.MarkerOptions() + ..draggable = true; controller.remove(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart index 6f2bf610f77d..e4c4dd7c0cfe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart @@ -5,7 +5,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:html' as html; -import 'dart:js_util' show getProperty; +import 'dart:typed_data'; +import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps/google_maps.dart' as gmaps; @@ -20,54 +21,56 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('MarkersController', () { - late StreamController events; + late StreamController> events; late MarkersController controller; late gmaps.GMap map; setUp(() { - events = StreamController(); + events = StreamController>(); controller = MarkersController(stream: events); map = gmaps.GMap(html.DivElement()); controller.bindToMap(123, map); }); testWidgets('addMarkers', (WidgetTester tester) async { - final markers = { - Marker(markerId: MarkerId('1')), - Marker(markerId: MarkerId('2')), + final Set markers = { + const Marker(markerId: MarkerId('1')), + const Marker(markerId: MarkerId('2')), }; controller.addMarkers(markers); expect(controller.markers.length, 2); - expect(controller.markers, contains(MarkerId('1'))); - expect(controller.markers, contains(MarkerId('2'))); - expect(controller.markers, isNot(contains(MarkerId('66')))); + expect(controller.markers, contains(const MarkerId('1'))); + expect(controller.markers, contains(const MarkerId('2'))); + expect(controller.markers, isNot(contains(const MarkerId('66')))); }); testWidgets('changeMarkers', (WidgetTester tester) async { - final markers = { - Marker(markerId: MarkerId('1')), + final Set markers = { + const Marker(markerId: MarkerId('1')), }; controller.addMarkers(markers); - expect(controller.markers[MarkerId('1')]?.marker?.draggable, isFalse); + expect( + controller.markers[const MarkerId('1')]?.marker?.draggable, isFalse); // Update the marker with radius 10 - final updatedMarkers = { - Marker(markerId: MarkerId('1'), draggable: true), + final Set updatedMarkers = { + const Marker(markerId: MarkerId('1'), draggable: true), }; controller.changeMarkers(updatedMarkers); expect(controller.markers.length, 1); - expect(controller.markers[MarkerId('1')]?.marker?.draggable, isTrue); + expect( + controller.markers[const MarkerId('1')]?.marker?.draggable, isTrue); }); testWidgets('removeMarkers', (WidgetTester tester) async { - final markers = { - Marker(markerId: MarkerId('1')), - Marker(markerId: MarkerId('2')), - Marker(markerId: MarkerId('3')), + final Set markers = { + const Marker(markerId: MarkerId('1')), + const Marker(markerId: MarkerId('2')), + const Marker(markerId: MarkerId('3')), }; controller.addMarkers(markers); @@ -75,102 +78,128 @@ void main() { expect(controller.markers.length, 3); // Remove some markers... - final markerIdsToRemove = { - MarkerId('1'), - MarkerId('3'), + final Set markerIdsToRemove = { + const MarkerId('1'), + const MarkerId('3'), }; controller.removeMarkers(markerIdsToRemove); expect(controller.markers.length, 1); - expect(controller.markers, isNot(contains(MarkerId('1')))); - expect(controller.markers, contains(MarkerId('2'))); - expect(controller.markers, isNot(contains(MarkerId('3')))); + expect(controller.markers, isNot(contains(const MarkerId('1')))); + expect(controller.markers, contains(const MarkerId('2'))); + expect(controller.markers, isNot(contains(const MarkerId('3')))); }); testWidgets('InfoWindow show/hide', (WidgetTester tester) async { - final markers = { - Marker( + final Set markers = { + const Marker( markerId: MarkerId('1'), - infoWindow: InfoWindow(title: "Title", snippet: "Snippet"), + infoWindow: InfoWindow(title: 'Title', snippet: 'Snippet'), ), }; controller.addMarkers(markers); - expect(controller.markers[MarkerId('1')]?.infoWindowShown, isFalse); + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); - controller.showMarkerInfoWindow(MarkerId('1')); + controller.showMarkerInfoWindow(const MarkerId('1')); - expect(controller.markers[MarkerId('1')]?.infoWindowShown, isTrue); + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isTrue); - controller.hideMarkerInfoWindow(MarkerId('1')); + controller.hideMarkerInfoWindow(const MarkerId('1')); - expect(controller.markers[MarkerId('1')]?.infoWindowShown, isFalse); + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); }); // https://github.com/flutter/flutter/issues/67380 testWidgets('only single InfoWindow is visible', (WidgetTester tester) async { - final markers = { - Marker( + final Set markers = { + const Marker( markerId: MarkerId('1'), - infoWindow: InfoWindow(title: "Title", snippet: "Snippet"), + infoWindow: InfoWindow(title: 'Title', snippet: 'Snippet'), ), - Marker( + const Marker( markerId: MarkerId('2'), - infoWindow: InfoWindow(title: "Title", snippet: "Snippet"), + infoWindow: InfoWindow(title: 'Title', snippet: 'Snippet'), ), }; controller.addMarkers(markers); - expect(controller.markers[MarkerId('1')]?.infoWindowShown, isFalse); - expect(controller.markers[MarkerId('2')]?.infoWindowShown, isFalse); + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); + expect(controller.markers[const MarkerId('2')]?.infoWindowShown, isFalse); - controller.showMarkerInfoWindow(MarkerId('1')); + controller.showMarkerInfoWindow(const MarkerId('1')); - expect(controller.markers[MarkerId('1')]?.infoWindowShown, isTrue); - expect(controller.markers[MarkerId('2')]?.infoWindowShown, isFalse); + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isTrue); + expect(controller.markers[const MarkerId('2')]?.infoWindowShown, isFalse); - controller.showMarkerInfoWindow(MarkerId('2')); + controller.showMarkerInfoWindow(const MarkerId('2')); - expect(controller.markers[MarkerId('1')]?.infoWindowShown, isFalse); - expect(controller.markers[MarkerId('2')]?.infoWindowShown, isTrue); + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); + expect(controller.markers[const MarkerId('2')]?.infoWindowShown, isTrue); }); // https://github.com/flutter/flutter/issues/66622 testWidgets('markers with custom bitmap icon work', (WidgetTester tester) async { - final bytes = Base64Decoder().convert(iconImageBase64); - final markers = { + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final Set markers = { Marker( - markerId: MarkerId('1'), icon: BitmapDescriptor.fromBytes(bytes)), + markerId: const MarkerId('1'), + icon: BitmapDescriptor.fromBytes(bytes), + ), }; controller.addMarkers(markers); expect(controller.markers.length, 1); - expect(controller.markers[MarkerId('1')]?.marker?.icon, isNotNull); - - final blobUrl = getProperty( - controller.markers[MarkerId('1')]!.marker!.icon!, - 'url', - ); + final gmaps.Icon? icon = + controller.markers[const MarkerId('1')]?.marker?.icon as gmaps.Icon?; + expect(icon, isNotNull); + final String blobUrl = icon!.url!; expect(blobUrl, startsWith('blob:')); - final response = await http.get(Uri.parse(blobUrl)); - + final http.Response response = await http.get(Uri.parse(blobUrl)); expect(response.bodyBytes, bytes, reason: 'Bytes from the Icon blob must match bytes used to create Marker'); }); + // https://github.com/flutter/flutter/issues/73789 + testWidgets('markers with custom bitmap icon pass size to sdk', + (WidgetTester tester) async { + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: BitmapDescriptor.fromBytes(bytes, size: const Size(20, 30)), + ), + }; + + controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final gmaps.Icon? icon = + controller.markers[const MarkerId('1')]?.marker?.icon as gmaps.Icon?; + expect(icon, isNotNull); + + final gmaps.Size size = icon!.size!; + final gmaps.Size scaledSize = icon.scaledSize!; + + expect(size.width, 20); + expect(size.height, 30); + expect(scaledSize.width, 20); + expect(scaledSize.height, 30); + }); + // https://github.com/flutter/flutter/issues/67854 testWidgets('InfoWindow snippet can have links', (WidgetTester tester) async { - final markers = { - Marker( + final Set markers = { + const Marker( markerId: MarkerId('1'), infoWindow: InfoWindow( title: 'title for test', @@ -182,19 +211,20 @@ void main() { controller.addMarkers(markers); expect(controller.markers.length, 1); - final content = controller.markers[MarkerId('1')]?.infoWindow?.content - as html.HtmlElement; - expect(content.innerHtml, contains('title for test')); + final html.HtmlElement? content = controller.markers[const MarkerId('1')] + ?.infoWindow?.content as html.HtmlElement?; + expect(content?.innerHtml, contains('title for test')); expect( - content.innerHtml, + content?.innerHtml, contains( - 'Go to Google >>>')); + 'Go to Google >>>', + )); }); // https://github.com/flutter/flutter/issues/67289 testWidgets('InfoWindow content is clickable', (WidgetTester tester) async { - final markers = { - Marker( + final Set markers = { + const Marker( markerId: MarkerId('1'), infoWindow: InfoWindow( title: 'title for test', @@ -206,15 +236,15 @@ void main() { controller.addMarkers(markers); expect(controller.markers.length, 1); - final content = controller.markers[MarkerId('1')]?.infoWindow?.content - as html.HtmlElement; + final html.HtmlElement? content = controller.markers[const MarkerId('1')] + ?.infoWindow?.content as html.HtmlElement?; - content.click(); + content?.click(); - final event = await events.stream.first; + final MapEvent event = await events.stream.first; expect(event, isA()); - expect((event as InfoWindowTapEvent).value, equals(MarkerId('1'))); + expect((event as InfoWindowTapEvent).value, equals(const MarkerId('1'))); }); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart index 8a5a62013538..a595a94655de 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart @@ -10,7 +10,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart' show GoogleMap, GoogleMapController; @@ -18,20 +17,20 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf import 'package:integration_test/integration_test.dart'; // This value is used when comparing long~num, like LatLng values. -const _acceptableLatLngDelta = 0.0000000001; +const double _acceptableLatLngDelta = 0.0000000001; // This value is used when comparing pixel measurements, mostly to gloss over // browser rounding errors. -const _acceptablePixelDelta = 1; +const int _acceptablePixelDelta = 1; /// Test Google Map Controller void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('Methods that require a proper Projection', () { - final LatLng center = LatLng(43.3078, -5.6958); - final Size size = Size(320, 240); - final CameraPosition initialCamera = CameraPosition( + const LatLng center = LatLng(43.3078, -5.6958); + const Size size = Size(320, 240); + const CameraPosition initialCamera = CameraPosition( target: center, zoom: 14, ); @@ -49,7 +48,7 @@ void main() { group('getScreenCoordinate', () { testWidgets('target of map is in center of widget', (WidgetTester tester) async { - pumpCenteredMap( + await pumpCenteredMap( tester, initialCamera: initialCamera, size: size, @@ -73,7 +72,7 @@ void main() { testWidgets('NorthWest of visible region corresponds to x:0, y:0', (WidgetTester tester) async { - pumpCenteredMap( + await pumpCenteredMap( tester, initialCamera: initialCamera, size: size, @@ -97,7 +96,7 @@ void main() { testWidgets( 'SouthEast of visible region corresponds to x:size.width, y:size.height', (WidgetTester tester) async { - pumpCenteredMap( + await pumpCenteredMap( tester, initialCamera: initialCamera, size: size, @@ -122,7 +121,7 @@ void main() { group('getLatLng', () { testWidgets('Center of widget is the target of map', (WidgetTester tester) async { - pumpCenteredMap( + await pumpCenteredMap( tester, initialCamera: initialCamera, size: size, @@ -147,7 +146,7 @@ void main() { testWidgets('Top-left of widget is NorthWest bound of map', (WidgetTester tester) async { - pumpCenteredMap( + await pumpCenteredMap( tester, initialCamera: initialCamera, size: size, @@ -162,7 +161,7 @@ void main() { ); final LatLng coords = await controller.getLatLng( - ScreenCoordinate(x: 0, y: 0), + const ScreenCoordinate(x: 0, y: 0), ); expect( @@ -177,7 +176,7 @@ void main() { testWidgets('Bottom-right of widget is SouthWest bound of map', (WidgetTester tester) async { - pumpCenteredMap( + await pumpCenteredMap( tester, initialCamera: initialCamera, size: size, @@ -209,16 +208,16 @@ void main() { } // Pumps a CenteredMap Widget into a given tester, with some parameters -void pumpCenteredMap( +Future pumpCenteredMap( WidgetTester tester, { required CameraPosition initialCamera, - Size size = const Size(320, 240), + Size? size, void Function(GoogleMapController)? onMapCreated, }) async { await tester.pumpWidget( CenteredMap( initialCamera: initialCamera, - size: size, + size: size ?? const Size(320, 240), onMapCreated: onMapCreated, ), ); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/resources/icon_image_base64.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/resources/icon_image_base64.dart index 6010f0107031..d08e96a65333 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/resources/icon_image_base64.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/resources/icon_image_base64.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -final iconImageBase64 = +const String iconImageBase64 = 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAIRlWElmTU' '0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIA' 'AIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQ' diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart index 547aaec6dc0a..11af181cffc2 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart @@ -4,10 +4,10 @@ import 'dart:async'; -import 'package:integration_test/integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps/google_maps.dart' as gmaps; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; /// Test Shapes (Circle, Polygon, Polyline) void main() { @@ -15,20 +15,20 @@ void main() { // Since onTap events happen asynchronously, we need to store when the event // is fired. We use a completer so the test can wait for the future to be completed. - late Completer _methodCalledCompleter; + late Completer methodCalledCompleter; - /// This is the future value of the [_methodCalledCompleter]. Reinitialized + /// This is the future value of the [methodCalledCompleter]. Reinitialized /// in the [setUp] method, and completed (as `true`) by [onTap], when it gets /// called by the corresponding Shape Controller. late Future methodCalled; void onTap() { - _methodCalledCompleter.complete(true); + methodCalledCompleter.complete(true); } setUp(() { - _methodCalledCompleter = Completer(); - methodCalled = _methodCalledCompleter.future; + methodCalledCompleter = Completer(); + methodCalled = methodCalledCompleter.future; }); group('CircleController', () { @@ -42,15 +42,16 @@ void main() { CircleController(circle: circle, consumeTapEvents: true, onTap: onTap); // Trigger a click event... - gmaps.Event.trigger(circle, 'click', [gmaps.MapMouseEvent()]); + gmaps.Event.trigger(circle, 'click', [gmaps.MapMouseEvent()]); // The event handling is now truly async. Wait for it... expect(await methodCalled, isTrue); }); testWidgets('update', (WidgetTester tester) async { - final controller = CircleController(circle: circle); - final options = gmaps.CircleOptions()..draggable = true; + final CircleController controller = CircleController(circle: circle); + final gmaps.CircleOptions options = gmaps.CircleOptions() + ..draggable = true; expect(circle.draggable, isNull); @@ -74,7 +75,8 @@ void main() { testWidgets('cannot call update after remove', (WidgetTester tester) async { - final options = gmaps.CircleOptions()..draggable = true; + final gmaps.CircleOptions options = gmaps.CircleOptions() + ..draggable = true; controller.remove(); @@ -96,15 +98,16 @@ void main() { PolygonController(polygon: polygon, consumeTapEvents: true, onTap: onTap); // Trigger a click event... - gmaps.Event.trigger(polygon, 'click', [gmaps.MapMouseEvent()]); + gmaps.Event.trigger(polygon, 'click', [gmaps.MapMouseEvent()]); // The event handling is now truly async. Wait for it... expect(await methodCalled, isTrue); }); testWidgets('update', (WidgetTester tester) async { - final controller = PolygonController(polygon: polygon); - final options = gmaps.PolygonOptions()..draggable = true; + final PolygonController controller = PolygonController(polygon: polygon); + final gmaps.PolygonOptions options = gmaps.PolygonOptions() + ..draggable = true; expect(polygon.draggable, isNull); @@ -128,7 +131,8 @@ void main() { testWidgets('cannot call update after remove', (WidgetTester tester) async { - final options = gmaps.PolygonOptions()..draggable = true; + final gmaps.PolygonOptions options = gmaps.PolygonOptions() + ..draggable = true; controller.remove(); @@ -148,18 +152,24 @@ void main() { testWidgets('onTap gets called', (WidgetTester tester) async { PolylineController( - polyline: polyline, consumeTapEvents: true, onTap: onTap); + polyline: polyline, + consumeTapEvents: true, + onTap: onTap, + ); // Trigger a click event... - gmaps.Event.trigger(polyline, 'click', [gmaps.MapMouseEvent()]); + gmaps.Event.trigger(polyline, 'click', [gmaps.MapMouseEvent()]); // The event handling is now truly async. Wait for it... expect(await methodCalled, isTrue); }); testWidgets('update', (WidgetTester tester) async { - final controller = PolylineController(polyline: polyline); - final options = gmaps.PolylineOptions()..draggable = true; + final PolylineController controller = PolylineController( + polyline: polyline, + ); + final gmaps.PolylineOptions options = gmaps.PolylineOptions() + ..draggable = true; expect(polyline.draggable, isNull); @@ -183,7 +193,8 @@ void main() { testWidgets('cannot call update after remove', (WidgetTester tester) async { - final options = gmaps.PolylineOptions()..draggable = true; + final gmaps.PolylineOptions options = gmaps.PolylineOptions() + ..draggable = true; controller.remove(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart index 80b4e0823bb5..b9bc2d371c9b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart @@ -3,20 +3,20 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui'; import 'dart:html' as html; +import 'dart:ui'; -import 'package:integration_test/integration_test.dart'; -import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps/google_maps.dart' as gmaps; import 'package:google_maps/google_maps_geometry.dart' as geometry; -import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:integration_test/integration_test.dart'; // This value is used when comparing the results of // converting from a byte value to a double between 0 and 1. // (For Color opacity values, for example) -const _acceptableDelta = 0.01; +const double _acceptableDelta = 0.01; /// Test Shapes (Circle, Polygon, Polyline) void main() { @@ -29,51 +29,51 @@ void main() { }); group('CirclesController', () { - late StreamController events; + late StreamController> events; late CirclesController controller; setUp(() { - events = StreamController(); + events = StreamController>(); controller = CirclesController(stream: events); controller.bindToMap(123, map); }); testWidgets('addCircles', (WidgetTester tester) async { - final circles = { - Circle(circleId: CircleId('1')), - Circle(circleId: CircleId('2')), + final Set circles = { + const Circle(circleId: CircleId('1')), + const Circle(circleId: CircleId('2')), }; controller.addCircles(circles); expect(controller.circles.length, 2); - expect(controller.circles, contains(CircleId('1'))); - expect(controller.circles, contains(CircleId('2'))); - expect(controller.circles, isNot(contains(CircleId('66')))); + expect(controller.circles, contains(const CircleId('1'))); + expect(controller.circles, contains(const CircleId('2'))); + expect(controller.circles, isNot(contains(const CircleId('66')))); }); testWidgets('changeCircles', (WidgetTester tester) async { - final circles = { - Circle(circleId: CircleId('1')), + final Set circles = { + const Circle(circleId: CircleId('1')), }; controller.addCircles(circles); - expect(controller.circles[CircleId('1')]?.circle?.visible, isTrue); + expect(controller.circles[const CircleId('1')]?.circle?.visible, isTrue); - final updatedCircles = { - Circle(circleId: CircleId('1'), visible: false), + final Set updatedCircles = { + const Circle(circleId: CircleId('1'), visible: false), }; controller.changeCircles(updatedCircles); expect(controller.circles.length, 1); - expect(controller.circles[CircleId('1')]?.circle?.visible, isFalse); + expect(controller.circles[const CircleId('1')]?.circle?.visible, isFalse); }); testWidgets('removeCircles', (WidgetTester tester) async { - final circles = { - Circle(circleId: CircleId('1')), - Circle(circleId: CircleId('2')), - Circle(circleId: CircleId('3')), + final Set circles = { + const Circle(circleId: CircleId('1')), + const Circle(circleId: CircleId('2')), + const Circle(circleId: CircleId('3')), }; controller.addCircles(circles); @@ -81,22 +81,22 @@ void main() { expect(controller.circles.length, 3); // Remove some circles... - final circleIdsToRemove = { - CircleId('1'), - CircleId('3'), + final Set circleIdsToRemove = { + const CircleId('1'), + const CircleId('3'), }; controller.removeCircles(circleIdsToRemove); expect(controller.circles.length, 1); - expect(controller.circles, isNot(contains(CircleId('1')))); - expect(controller.circles, contains(CircleId('2'))); - expect(controller.circles, isNot(contains(CircleId('3')))); + expect(controller.circles, isNot(contains(const CircleId('1')))); + expect(controller.circles, contains(const CircleId('2'))); + expect(controller.circles, isNot(contains(const CircleId('3')))); }); testWidgets('Converts colors to CSS', (WidgetTester tester) async { - final circles = { - Circle( + final Set circles = { + const Circle( circleId: CircleId('1'), fillColor: Color(0x7FFABADA), strokeColor: Color(0xFFC0FFEE), @@ -105,7 +105,7 @@ void main() { controller.addCircles(circles); - final circle = controller.circles.values.first.circle!; + final gmaps.Circle circle = controller.circles.values.first.circle!; expect(circle.get('fillColor'), '#fabada'); expect(circle.get('fillOpacity'), closeTo(0.5, _acceptableDelta)); @@ -115,52 +115,54 @@ void main() { }); group('PolygonsController', () { - late StreamController events; + late StreamController> events; late PolygonsController controller; setUp(() { - events = StreamController(); + events = StreamController>(); controller = PolygonsController(stream: events); controller.bindToMap(123, map); }); testWidgets('addPolygons', (WidgetTester tester) async { - final polygons = { - Polygon(polygonId: PolygonId('1')), - Polygon(polygonId: PolygonId('2')), + final Set polygons = { + const Polygon(polygonId: PolygonId('1')), + const Polygon(polygonId: PolygonId('2')), }; controller.addPolygons(polygons); expect(controller.polygons.length, 2); - expect(controller.polygons, contains(PolygonId('1'))); - expect(controller.polygons, contains(PolygonId('2'))); - expect(controller.polygons, isNot(contains(PolygonId('66')))); + expect(controller.polygons, contains(const PolygonId('1'))); + expect(controller.polygons, contains(const PolygonId('2'))); + expect(controller.polygons, isNot(contains(const PolygonId('66')))); }); testWidgets('changePolygons', (WidgetTester tester) async { - final polygons = { - Polygon(polygonId: PolygonId('1')), + final Set polygons = { + const Polygon(polygonId: PolygonId('1')), }; controller.addPolygons(polygons); - expect(controller.polygons[PolygonId('1')]?.polygon?.visible, isTrue); + expect( + controller.polygons[const PolygonId('1')]?.polygon?.visible, isTrue); // Update the polygon - final updatedPolygons = { - Polygon(polygonId: PolygonId('1'), visible: false), + final Set updatedPolygons = { + const Polygon(polygonId: PolygonId('1'), visible: false), }; controller.changePolygons(updatedPolygons); expect(controller.polygons.length, 1); - expect(controller.polygons[PolygonId('1')]?.polygon?.visible, isFalse); + expect( + controller.polygons[const PolygonId('1')]?.polygon?.visible, isFalse); }); testWidgets('removePolygons', (WidgetTester tester) async { - final polygons = { - Polygon(polygonId: PolygonId('1')), - Polygon(polygonId: PolygonId('2')), - Polygon(polygonId: PolygonId('3')), + final Set polygons = { + const Polygon(polygonId: PolygonId('1')), + const Polygon(polygonId: PolygonId('2')), + const Polygon(polygonId: PolygonId('3')), }; controller.addPolygons(polygons); @@ -168,22 +170,22 @@ void main() { expect(controller.polygons.length, 3); // Remove some polygons... - final polygonIdsToRemove = { - PolygonId('1'), - PolygonId('3'), + final Set polygonIdsToRemove = { + const PolygonId('1'), + const PolygonId('3'), }; controller.removePolygons(polygonIdsToRemove); expect(controller.polygons.length, 1); - expect(controller.polygons, isNot(contains(PolygonId('1')))); - expect(controller.polygons, contains(PolygonId('2'))); - expect(controller.polygons, isNot(contains(PolygonId('3')))); + expect(controller.polygons, isNot(contains(const PolygonId('1')))); + expect(controller.polygons, contains(const PolygonId('2'))); + expect(controller.polygons, isNot(contains(const PolygonId('3')))); }); testWidgets('Converts colors to CSS', (WidgetTester tester) async { - final polygons = { - Polygon( + final Set polygons = { + const Polygon( polygonId: PolygonId('1'), fillColor: Color(0x7FFABADA), strokeColor: Color(0xFFC0FFEE), @@ -192,7 +194,7 @@ void main() { controller.addPolygons(polygons); - final polygon = controller.polygons.values.first.polygon!; + final gmaps.Polygon polygon = controller.polygons.values.first.polygon!; expect(polygon.get('fillColor'), '#fabada'); expect(polygon.get('fillOpacity'), closeTo(0.5, _acceptableDelta)); @@ -201,16 +203,16 @@ void main() { }); testWidgets('Handle Polygons with holes', (WidgetTester tester) async { - final polygons = { - Polygon( + final Set polygons = { + const Polygon( polygonId: PolygonId('BermudaTriangle'), - points: [ + points: [ LatLng(25.774, -80.19), LatLng(18.466, -66.118), LatLng(32.321, -64.757), ], - holes: [ - [ + holes: >[ + [ LatLng(28.745, -70.579), LatLng(29.57, -67.514), LatLng(27.339, -66.668), @@ -222,21 +224,21 @@ void main() { controller.addPolygons(polygons); expect(controller.polygons.length, 1); - expect(controller.polygons, contains(PolygonId('BermudaTriangle'))); - expect(controller.polygons, isNot(contains(PolygonId('66')))); + expect(controller.polygons, contains(const PolygonId('BermudaTriangle'))); + expect(controller.polygons, isNot(contains(const PolygonId('66')))); }); testWidgets('Polygon with hole has a hole', (WidgetTester tester) async { - final polygons = { - Polygon( + final Set polygons = { + const Polygon( polygonId: PolygonId('BermudaTriangle'), - points: [ + points: [ LatLng(25.774, -80.19), LatLng(18.466, -66.118), LatLng(32.321, -64.757), ], - holes: [ - [ + holes: >[ + [ LatLng(28.745, -70.579), LatLng(29.57, -67.514), LatLng(27.339, -66.668), @@ -247,24 +249,24 @@ void main() { controller.addPolygons(polygons); - final polygon = controller.polygons.values.first.polygon; - final pointInHole = gmaps.LatLng(28.632, -68.401); + final gmaps.Polygon? polygon = controller.polygons.values.first.polygon; + final gmaps.LatLng pointInHole = gmaps.LatLng(28.632, -68.401); expect(geometry.Poly.containsLocation(pointInHole, polygon), false); }); testWidgets('Hole Path gets reversed to display correctly', (WidgetTester tester) async { - final polygons = { - Polygon( + final Set polygons = { + const Polygon( polygonId: PolygonId('BermudaTriangle'), - points: [ + points: [ LatLng(25.774, -80.19), LatLng(18.466, -66.118), LatLng(32.321, -64.757), ], - holes: [ - [ + holes: >[ + [ LatLng(27.339, -66.668), LatLng(29.57, -67.514), LatLng(28.745, -70.579), @@ -275,7 +277,8 @@ void main() { controller.addPolygons(polygons); - final paths = controller.polygons.values.first.polygon!.paths!; + final gmaps.MVCArray?> paths = + controller.polygons.values.first.polygon!.paths!; expect(paths.getAt(1)?.getAt(0)?.lat, 28.745); expect(paths.getAt(1)?.getAt(1)?.lat, 29.57); @@ -284,51 +287,51 @@ void main() { }); group('PolylinesController', () { - late StreamController events; + late StreamController> events; late PolylinesController controller; setUp(() { - events = StreamController(); + events = StreamController>(); controller = PolylinesController(stream: events); controller.bindToMap(123, map); }); testWidgets('addPolylines', (WidgetTester tester) async { - final polylines = { - Polyline(polylineId: PolylineId('1')), - Polyline(polylineId: PolylineId('2')), + final Set polylines = { + const Polyline(polylineId: PolylineId('1')), + const Polyline(polylineId: PolylineId('2')), }; controller.addPolylines(polylines); expect(controller.lines.length, 2); - expect(controller.lines, contains(PolylineId('1'))); - expect(controller.lines, contains(PolylineId('2'))); - expect(controller.lines, isNot(contains(PolylineId('66')))); + expect(controller.lines, contains(const PolylineId('1'))); + expect(controller.lines, contains(const PolylineId('2'))); + expect(controller.lines, isNot(contains(const PolylineId('66')))); }); testWidgets('changePolylines', (WidgetTester tester) async { - final polylines = { - Polyline(polylineId: PolylineId('1')), + final Set polylines = { + const Polyline(polylineId: PolylineId('1')), }; controller.addPolylines(polylines); - expect(controller.lines[PolylineId('1')]?.line?.visible, isTrue); + expect(controller.lines[const PolylineId('1')]?.line?.visible, isTrue); - final updatedPolylines = { - Polyline(polylineId: PolylineId('1'), visible: false), + final Set updatedPolylines = { + const Polyline(polylineId: PolylineId('1'), visible: false), }; controller.changePolylines(updatedPolylines); expect(controller.lines.length, 1); - expect(controller.lines[PolylineId('1')]?.line?.visible, isFalse); + expect(controller.lines[const PolylineId('1')]?.line?.visible, isFalse); }); testWidgets('removePolylines', (WidgetTester tester) async { - final polylines = { - Polyline(polylineId: PolylineId('1')), - Polyline(polylineId: PolylineId('2')), - Polyline(polylineId: PolylineId('3')), + final Set polylines = { + const Polyline(polylineId: PolylineId('1')), + const Polyline(polylineId: PolylineId('2')), + const Polyline(polylineId: PolylineId('3')), }; controller.addPolylines(polylines); @@ -336,22 +339,22 @@ void main() { expect(controller.lines.length, 3); // Remove some polylines... - final polylineIdsToRemove = { - PolylineId('1'), - PolylineId('3'), + final Set polylineIdsToRemove = { + const PolylineId('1'), + const PolylineId('3'), }; controller.removePolylines(polylineIdsToRemove); expect(controller.lines.length, 1); - expect(controller.lines, isNot(contains(PolylineId('1')))); - expect(controller.lines, contains(PolylineId('2'))); - expect(controller.lines, isNot(contains(PolylineId('3')))); + expect(controller.lines, isNot(contains(const PolylineId('1')))); + expect(controller.lines, contains(const PolylineId('2'))); + expect(controller.lines, isNot(contains(const PolylineId('3')))); }); testWidgets('Converts colors to CSS', (WidgetTester tester) async { - final lines = { - Polyline( + final Set lines = { + const Polyline( polylineId: PolylineId('1'), color: Color(0x7FFABADA), ), @@ -359,7 +362,7 @@ void main() { controller.addPolylines(lines); - final line = controller.lines.values.first.line!; + final gmaps.Polyline line = controller.lines.values.first.line!; expect(line.get('strokeColor'), '#fabada'); expect(line.get('strokeOpacity'), closeTo(0.5, _acceptableDelta)); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/lib/main.dart index 10415204570c..e93a60e19906 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/lib/main.dart @@ -5,18 +5,21 @@ import 'package:flutter/material.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } /// App for testing class MyApp extends StatefulWidget { + /// Constructor with key + const MyApp({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { @override Widget build(BuildContext context) { - return Text('Testing... Look at the console output for results!'); + return const Text('Testing... Look at the console output for results!'); } } 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 95a3d4253440..35c9c903e982 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,24 +4,25 @@ publish_to: none # Tests require flutter beta or greater to run. environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.1.0" + flutter: ">=2.10.0" dependencies: - google_maps_flutter_web: - path: ../ flutter: sdk: flutter + google_maps_flutter_platform_interface: ^2.2.1 + google_maps_flutter_web: + path: ../ dev_dependencies: build_runner: ^2.1.1 - google_maps: ^5.2.0 - google_maps_flutter: # Used for projection_test.dart - path: ../../google_maps_flutter - http: ^0.13.0 - mockito: ^5.0.0 flutter_driver: sdk: flutter flutter_test: sdk: flutter + google_maps: ^6.1.0 + google_maps_flutter: # Used for projection_test.dart + path: ../../google_maps_flutter + http: ^0.13.0 integration_test: sdk: flutter + mockito: ^5.3.2 diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart index 0355f2923528..0650184a14d0 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart @@ -5,37 +5,32 @@ library google_maps_flutter_web; import 'dart:async'; +import 'dart:convert'; import 'dart:html'; import 'dart:js_util'; -import 'src/shims/dart_ui.dart' as ui; // Conditionally imports dart:ui in web -import 'dart:convert'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/gestures.dart'; - -import 'package:sanitize_html/sanitize_html.dart'; - -import 'package:stream_transform/stream_transform.dart'; - -import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:sanitize_html/sanitize_html.dart'; +import 'package:stream_transform/stream_transform.dart'; +import 'src/shims/dart_ui.dart' as ui; // Conditionally imports dart:ui in web import 'src/third_party/to_screen_location/to_screen_location.dart'; import 'src/types.dart'; -part 'src/google_maps_flutter_web.dart'; -part 'src/google_maps_controller.dart'; part 'src/circle.dart'; part 'src/circles.dart'; +part 'src/convert.dart'; +part 'src/google_maps_controller.dart'; +part 'src/google_maps_flutter_web.dart'; +part 'src/marker.dart'; +part 'src/markers.dart'; part 'src/polygon.dart'; part 'src/polygons.dart'; part 'src/polyline.dart'; part 'src/polylines.dart'; -part 'src/marker.dart'; -part 'src/markers.dart'; -part 'src/convert.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.dart index 65057d8c869e..9cd3ba1c079c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.dart @@ -6,10 +6,6 @@ part of google_maps_flutter_web; /// The `CircleController` class wraps a [gmaps.Circle] and its `onTap` behavior. class CircleController { - gmaps.Circle? _circle; - - final bool _consumeTapEvents; - /// Creates a `CircleController`, which wraps a [gmaps.Circle] object and its `onTap` behavior. CircleController({ required gmaps.Circle circle, @@ -24,6 +20,10 @@ class CircleController { } } + gmaps.Circle? _circle; + + final bool _consumeTapEvents; + /// Returns the wrapped [gmaps.Circle]. Only used for testing. @visibleForTesting gmaps.Circle? get circle => _circle; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart index ae8faa038ea6..bc6eac14200f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.dart @@ -6,17 +6,17 @@ part of google_maps_flutter_web; /// This class manages all the [CircleController]s associated to a [GoogleMapController]. class CirclesController extends GeometryController { + /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. + CirclesController({ + required StreamController> stream, + }) : _streamController = stream, + _circleIdToController = {}; + // A cache of [CircleController]s indexed by their [CircleId]. final Map _circleIdToController; // The stream over which circles broadcast their events - StreamController _streamController; - - /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. - CirclesController({ - required StreamController stream, - }) : _streamController = stream, - _circleIdToController = Map(); + final StreamController> _streamController; /// Returns the cache of [CircleController]s. Test only. @visibleForTesting @@ -26,9 +26,7 @@ class CirclesController extends GeometryController { /// /// Wraps each [Circle] into its corresponding [CircleController]. void addCircles(Set circlesToAdd) { - circlesToAdd.forEach((circle) { - _addCircle(circle); - }); + circlesToAdd.forEach(_addCircle); } void _addCircle(Circle circle) { @@ -36,10 +34,9 @@ class CirclesController extends GeometryController { return; } - final populationOptions = _circleOptionsFromCircle(circle); - gmaps.Circle gmCircle = gmaps.Circle(populationOptions); - gmCircle.map = googleMap; - CircleController controller = CircleController( + final gmaps.CircleOptions circleOptions = _circleOptionsFromCircle(circle); + final gmaps.Circle gmCircle = gmaps.Circle(circleOptions)..map = googleMap; + final CircleController controller = CircleController( circle: gmCircle, consumeTapEvents: circle.consumeTapEvents, onTap: () { @@ -50,24 +47,25 @@ class CirclesController extends GeometryController { /// Updates a set of [Circle] objects with new options. void changeCircles(Set circlesToChange) { - circlesToChange.forEach((circleToChange) { - _changeCircle(circleToChange); - }); + circlesToChange.forEach(_changeCircle); } void _changeCircle(Circle circle) { - final circleController = _circleIdToController[circle.circleId]; + final CircleController? circleController = + _circleIdToController[circle.circleId]; circleController?.update(_circleOptionsFromCircle(circle)); } /// Removes a set of [CircleId]s from the cache. void removeCircles(Set circleIdsToRemove) { - circleIdsToRemove.forEach((circleId) { - final CircleController? circleController = - _circleIdToController[circleId]; - circleController?.remove(); - _circleIdToController.remove(circleId); - }); + circleIdsToRemove.forEach(_removeCircle); + } + + // Removes a circle and its controller by its [CircleId]. + void _removeCircle(CircleId circleId) { + final CircleController? circleController = _circleIdToController[circleId]; + circleController?.remove(); + _circleIdToController.remove(circleId); } // Handles the global onCircleTap function to funnel events from circles into the stream. 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 c026a03be804..2b09950cc00d 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 @@ -5,30 +5,20 @@ part of google_maps_flutter_web; // Default values for when the gmaps objects return null/undefined values. -final _nullGmapsLatLng = gmaps.LatLng(0, 0); -final _nullGmapsLatLngBounds = +final gmaps.LatLng _nullGmapsLatLng = gmaps.LatLng(0, 0); +final gmaps.LatLngBounds _nullGmapsLatLngBounds = gmaps.LatLngBounds(_nullGmapsLatLng, _nullGmapsLatLng); // Defaults taken from the Google Maps Platform SDK documentation. -final _defaultCssColor = '#000000'; -final _defaultCssOpacity = 0.0; - -// Indices in the plugin side don't match with the ones -// in the gmaps lib. This translates from plugin -> gmaps. -final _mapTypeToMapTypeId = { - 0: gmaps.MapTypeId.ROADMAP, // "none" in the plugin - 1: gmaps.MapTypeId.ROADMAP, - 2: gmaps.MapTypeId.SATELLITE, - 3: gmaps.MapTypeId.TERRAIN, - 4: gmaps.MapTypeId.HYBRID, -}; +const String _defaultCssColor = '#000000'; +const double _defaultCssOpacity = 0.0; // Converts a [Color] into a valid CSS value #RRGGBB. String _getCssColor(Color color) { if (color == null) { return _defaultCssColor; } - return '#' + color.value.toRadixString(16).padLeft(8, '0').substring(2); + return '#${color.value.toRadixString(16).padLeft(8, '0').substring(2)}'; } // Extracts the opacity from a [Color]. @@ -55,47 +45,64 @@ double _getCssOpacity(Color color) { // indoorViewEnabled seems to not have an equivalent in web // buildingsEnabled seems to not have an equivalent in web // padding seems to behave differently in web than mobile. You can't move UI elements in web. -gmaps.MapOptions _rawOptionsToGmapsOptions(Map rawOptions) { - gmaps.MapOptions options = gmaps.MapOptions(); +gmaps.MapOptions _configurationAndStyleToGmapsOptions( + MapConfiguration configuration, List styles) { + final gmaps.MapOptions options = gmaps.MapOptions(); - if (_mapTypeToMapTypeId.containsKey(rawOptions['mapType'])) { - options.mapTypeId = _mapTypeToMapTypeId[rawOptions['mapType']]; + if (configuration.mapType != null) { + options.mapTypeId = _gmapTypeIDForPluginType(configuration.mapType!); } - if (rawOptions['minMaxZoomPreference'] != null) { + final MinMaxZoomPreference? zoomPreference = + configuration.minMaxZoomPreference; + if (zoomPreference != null) { options - ..minZoom = rawOptions['minMaxZoomPreference'][0] - ..maxZoom = rawOptions['minMaxZoomPreference'][1]; + ..minZoom = zoomPreference.minZoom + ..maxZoom = zoomPreference.maxZoom; } - if (rawOptions['cameraTargetBounds'] != null) { + if (configuration.cameraTargetBounds != null) { // Needs gmaps.MapOptions.restriction and gmaps.MapRestriction // see: https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions.restriction } - if (rawOptions['zoomControlsEnabled'] != null) { - options.zoomControl = rawOptions['zoomControlsEnabled']; - } - - if (rawOptions['styles'] != null) { - options.styles = rawOptions['styles']; + if (configuration.zoomControlsEnabled != null) { + options.zoomControl = configuration.zoomControlsEnabled; } - if (rawOptions['scrollGesturesEnabled'] == false || - rawOptions['zoomGesturesEnabled'] == false) { + if (configuration.scrollGesturesEnabled == false || + configuration.zoomGesturesEnabled == false) { options.gestureHandling = 'none'; } else { options.gestureHandling = 'auto'; } - // These don't have any rawOptions entry, but they seem to be off in the native maps. + // These don't have any configuration entries, but they seem to be off in the + // native maps. options.mapTypeControl = false; options.fullscreenControl = false; options.streetViewControl = false; + options.styles = styles; + return options; } +gmaps.MapTypeId _gmapTypeIDForPluginType(MapType type) { + switch (type) { + case MapType.satellite: + return gmaps.MapTypeId.SATELLITE; + case MapType.terrain: + return gmaps.MapTypeId.TERRAIN; + case MapType.hybrid: + return gmaps.MapTypeId.HYBRID; + case MapType.normal: + case MapType.none: + default: + return gmaps.MapTypeId.ROADMAP; + } +} + gmaps.MapOptions _applyInitialPosition( CameraPosition initialPosition, gmaps.MapOptions options, @@ -109,38 +116,38 @@ gmaps.MapOptions _applyInitialPosition( return options; } -// Extracts the status of the traffic layer from the rawOptions map. -bool _isTrafficLayerEnabled(Map rawOptions) { - return rawOptions['trafficEnabled'] ?? false; -} - // The keys we'd expect to see in a serialized MapTypeStyle JSON object. -final _mapStyleKeys = { +final Set _mapStyleKeys = { 'elementType', 'featureType', 'stylers', }; // Checks if the passed in Map contains some of the _mapStyleKeys. -bool _isJsonMapStyle(Map value) { +bool _isJsonMapStyle(Map value) { return _mapStyleKeys.intersection(value.keys.toSet()).isNotEmpty; } // Converts an incoming JSON-encoded Style info, into the correct gmaps array. List _mapStyles(String? mapStyleJson) { - List styles = []; + List styles = []; if (mapStyleJson != null) { - styles = json - .decode(mapStyleJson, reviver: (key, value) { - if (value is Map && _isJsonMapStyle(value)) { - return gmaps.MapTypeStyle() - ..elementType = value['elementType'] - ..featureType = value['featureType'] - ..stylers = - (value['stylers'] as List).map((e) => jsify(e)).toList(); - } - return value; - }) + styles = (json.decode(mapStyleJson, reviver: (Object? key, Object? value) { + if (value is Map && _isJsonMapStyle(value as Map)) { + List stylers = []; + if (value['stylers'] != null) { + stylers = (value['stylers']! as List) + .map((Object? e) => e != null ? jsify(e) : null) + .toList(); + } + return gmaps.MapTypeStyle() + ..elementType = value['elementType'] as String? + ..featureType = value['featureType'] as String? + ..stylers = stylers; + } + return value; + }) as List) + .where((Object? element) => element != null) .cast() .toList(); // .toList calls are required so the JS API understands the underlying data structure. @@ -173,12 +180,12 @@ CameraPosition _gmViewportToCameraPosition(gmaps.GMap map) { } // Convert plugin objects to gmaps.Options objects -// TODO: Move to their appropriate objects, maybe make these copy constructors: +// TODO(ditman): Move to their appropriate objects, maybe make them copy constructors? // Marker.fromMarker(anotherMarker, moreOptions); gmaps.InfoWindowOptions? _infoWindowOptionsFromMarker(Marker marker) { - final markerTitle = marker.infoWindow.title ?? ''; - final markerSnippet = marker.infoWindow.snippet ?? ''; + final String markerTitle = marker.infoWindow.title ?? ''; + final String markerSnippet = marker.infoWindow.snippet ?? ''; // If both the title and snippet of an infowindow are empty, we don't really // want an infowindow... @@ -200,6 +207,13 @@ gmaps.InfoWindowOptions? _infoWindowOptionsFromMarker(Marker marker) { if (markerSnippet.isNotEmpty) { final HtmlElement snippet = DivElement() ..className = 'infowindow-snippet' + // `sanitizeHtml` is used to clean the (potential) user input from (potential) + // XSS attacks through the contents of the marker InfoWindow. + // See: https://pub.dev/documentation/sanitize_html/latest/sanitize_html/sanitizeHtml.html + // See: b/159137885, b/159598165 + // The NodeTreeSanitizer.trusted just tells setInnerHtml to leave the output + // of `sanitizeHtml` untouched. + // ignore: unsafe_html ..setInnerHtml( sanitizeHtml(markerSnippet), treeSanitizer: NodeTreeSanitizer.trusted, @@ -210,18 +224,29 @@ gmaps.InfoWindowOptions? _infoWindowOptionsFromMarker(Marker marker) { return gmaps.InfoWindowOptions() ..content = container ..zIndex = marker.zIndex; - // TODO: Compute the pixelOffset of the infoWindow, from the size of the Marker, + // TODO(ditman): Compute the pixelOffset of the infoWindow, from the size of the Marker, // and the marker.infoWindow.anchor property. } -// Computes the options for a new [gmaps.Marker] from an incoming set of options -// [marker], and the existing marker registered with the map: [currentMarker]. -// Preserves the position from the [currentMarker], if set. -gmaps.MarkerOptions _markerOptionsFromMarker( - Marker marker, - gmaps.Marker? currentMarker, -) { - final iconConfig = marker.icon.toJson() as List; +// Attempts to extract a [gmaps.Size] from `iconConfig[sizeIndex]`. +gmaps.Size? _gmSizeFromIconConfig(List iconConfig, int sizeIndex) { + gmaps.Size? size; + if (iconConfig.length >= sizeIndex + 1) { + final List? rawIconSize = iconConfig[sizeIndex] as List?; + if (rawIconSize != null) { + size = gmaps.Size( + rawIconSize[0] as num?, + rawIconSize[1] as num?, + ); + } + } + return size; +} + +// Converts a [BitmapDescriptor] into a [gmaps.Icon] that can be used in Markers. +gmaps.Icon? _gmIconFromBitmapDescriptor(BitmapDescriptor bitmapDescriptor) { + final List iconConfig = bitmapDescriptor.toJson() as List; + gmaps.Icon? icon; if (iconConfig != null) { @@ -229,42 +254,59 @@ gmaps.MarkerOptions _markerOptionsFromMarker( assert(iconConfig.length >= 2); // iconConfig[2] contains the DPIs of the screen, but that information is // already encoded in the iconConfig[1] - icon = gmaps.Icon() - ..url = ui.webOnlyAssetManager.getAssetUrl(iconConfig[1]); + ..url = ui.webOnlyAssetManager.getAssetUrl(iconConfig[1]! as String); - // iconConfig[3] may contain the [width, height] of the image, if passed! - if (iconConfig.length >= 4 && iconConfig[3] != null) { - final size = gmaps.Size(iconConfig[3][0], iconConfig[3][1]); + final gmaps.Size? size = _gmSizeFromIconConfig(iconConfig, 3); + if (size != null) { icon ..size = size ..scaledSize = size; } } else if (iconConfig[0] == 'fromBytes') { // Grab the bytes, and put them into a blob - List bytes = iconConfig[1]; - final blob = Blob([bytes]); // Let the browser figure out the encoding + final List bytes = iconConfig[1]! as List; + // Create a Blob from bytes, but let the browser figure out the encoding + final Blob blob = Blob([bytes]); icon = gmaps.Icon()..url = Url.createObjectUrlFromBlob(blob); + + final gmaps.Size? size = _gmSizeFromIconConfig(iconConfig, 2); + if (size != null) { + icon + ..size = size + ..scaledSize = size; + } } } + + return icon; +} + +// Computes the options for a new [gmaps.Marker] from an incoming set of options +// [marker], and the existing marker registered with the map: [currentMarker]. +// Preserves the position from the [currentMarker], if set. +gmaps.MarkerOptions _markerOptionsFromMarker( + Marker marker, + gmaps.Marker? currentMarker, +) { return gmaps.MarkerOptions() ..position = currentMarker?.position ?? gmaps.LatLng( marker.position.latitude, marker.position.longitude, ) - ..title = sanitizeHtml(marker.infoWindow.title ?? "") + ..title = sanitizeHtml(marker.infoWindow.title ?? '') ..zIndex = marker.zIndex ..visible = marker.visible ..opacity = marker.alpha ..draggable = marker.draggable - ..icon = icon; - // TODO: Compute anchor properly, otherwise infowindows attach to the wrong spot. + ..icon = _gmIconFromBitmapDescriptor(marker.icon); + // TODO(ditman): Compute anchor properly, otherwise infowindows attach to the wrong spot. // Flat and Rotation are not supported directly on the web. } gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { - final circleOptions = gmaps.CircleOptions() + final gmaps.CircleOptions circleOptions = gmaps.CircleOptions() ..strokeColor = _getCssColor(circle.strokeColor) ..strokeOpacity = _getCssOpacity(circle.strokeColor) ..strokeWeight = circle.strokeWidth @@ -279,28 +321,25 @@ gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { gmaps.PolygonOptions _polygonOptionsFromPolygon( gmaps.GMap googleMap, Polygon polygon) { - List path = []; - polygon.points.forEach((point) { - path.add(_latLngToGmLatLng(point)); - }); - final polygonDirection = _isPolygonClockwise(path); - List> paths = [path]; - int holeIndex = 0; - polygon.holes.forEach((hole) { - List holePath = - hole.map((point) => _latLngToGmLatLng(point)).toList(); - if (_isPolygonClockwise(holePath) == polygonDirection) { - holePath = holePath.reversed.toList(); - if (kDebugMode) { - print( - 'Hole [$holeIndex] in Polygon [${polygon.polygonId.value}] has been reversed.' - ' Ensure holes in polygons are "wound in the opposite direction to the outer path."' - ' More info: https://github.com/flutter/flutter/issues/74096'); - } - } - paths.add(holePath); - holeIndex++; - }); + // Convert all points to GmLatLng + final List path = + polygon.points.map(_latLngToGmLatLng).toList(); + + final bool isClockwisePolygon = _isPolygonClockwise(path); + + final List> paths = >[path]; + + for (int i = 0; i < polygon.holes.length; i++) { + final List hole = polygon.holes[i]; + final List correctHole = _ensureHoleHasReverseWinding( + hole, + isClockwisePolygon, + holeId: i, + polygonId: polygon.polygonId, + ); + paths.add(correctHole); + } + return gmaps.PolygonOptions() ..paths = paths ..strokeColor = _getCssColor(polygon.strokeColor) @@ -313,6 +352,27 @@ gmaps.PolygonOptions _polygonOptionsFromPolygon( ..geodesic = polygon.geodesic; } +List _ensureHoleHasReverseWinding( + List hole, + bool polyIsClockwise, { + required int holeId, + required PolygonId polygonId, +}) { + List holePath = hole.map(_latLngToGmLatLng).toList(); + final bool holeIsClockwise = _isPolygonClockwise(holePath); + + if (holeIsClockwise == polyIsClockwise) { + holePath = holePath.reversed.toList(); + if (kDebugMode) { + print('Hole [$holeId] in Polygon [${polygonId.value}] has been reversed.' + ' Ensure holes in polygons are "wound in the opposite direction to the outer path."' + ' More info: https://github.com/flutter/flutter/issues/74096'); + } + } + + return holePath; +} + /// Calculates the direction of a given Polygon /// based on: https://stackoverflow.com/a/1165943 /// @@ -325,8 +385,8 @@ gmaps.PolygonOptions _polygonOptionsFromPolygon( /// the `path` is a transformed version of [Polygon.points] or each of the /// [Polygon.holes], guaranteeing that `lat` and `lng` can be accessed with `!`. bool _isPolygonClockwise(List path) { - var direction = 0.0; - for (var i = 0; i < path.length; i++) { + double direction = 0.0; + for (int i = 0; i < path.length; i++) { direction = direction + ((path[(i + 1) % path.length].lat - path[i].lat) * (path[(i + 1) % path.length].lng + path[i].lng)); @@ -336,10 +396,8 @@ bool _isPolygonClockwise(List path) { gmaps.PolylineOptions _polylineOptionsFromPolyline( gmaps.GMap googleMap, Polyline polyline) { - List paths = []; - polyline.points.forEach((point) { - paths.add(_latLngToGmLatLng(point)); - }); + final List paths = + polyline.points.map(_latLngToGmLatLng).toList(); return gmaps.PolylineOptions() ..path = paths @@ -358,40 +416,50 @@ gmaps.PolylineOptions _polylineOptionsFromPolyline( // Translates a [CameraUpdate] into operations on a [gmaps.GMap]. void _applyCameraUpdate(gmaps.GMap map, CameraUpdate update) { - final json = update.toJson() as List; + final List json = update.toJson() as List; switch (json[0]) { case 'newCameraPosition': - map.heading = json[1]['bearing']; - map.zoom = json[1]['zoom']; - map.panTo(gmaps.LatLng(json[1]['target'][0], json[1]['target'][1])); - map.tilt = json[1]['tilt']; + map.heading = json[1]['bearing'] as num?; + map.zoom = json[1]['zoom'] as num?; + map.panTo( + gmaps.LatLng( + json[1]['target'][0] as num?, + json[1]['target'][1] as num?, + ), + ); + map.tilt = json[1]['tilt'] as num?; break; case 'newLatLng': - map.panTo(gmaps.LatLng(json[1][0], json[1][1])); + map.panTo(gmaps.LatLng(json[1][0] as num?, json[1][1] as num?)); break; case 'newLatLngZoom': - map.zoom = json[2]; - map.panTo(gmaps.LatLng(json[1][0], json[1][1])); + map.zoom = json[2] as num?; + map.panTo(gmaps.LatLng(json[1][0] as num?, json[1][1] as num?)); break; case 'newLatLngBounds': - map.fitBounds(gmaps.LatLngBounds( - gmaps.LatLng(json[1][0][0], json[1][0][1]), - gmaps.LatLng(json[1][1][0], json[1][1][1]))); + map.fitBounds( + gmaps.LatLngBounds( + gmaps.LatLng(json[1][0][0] as num?, json[1][0][1] as num?), + gmaps.LatLng(json[1][1][0] as num?, json[1][1][1] as num?), + ), + ); // padding = json[2]; // Needs package:google_maps ^4.0.0 to adjust the padding in fitBounds break; case 'scrollBy': - map.panBy(json[1], json[2]); + map.panBy(json[1] as num?, json[2] as num?); break; case 'zoomBy': gmaps.LatLng? focusLatLng; - double zoomDelta = json[1] ?? 0; + final double zoomDelta = json[1] as double? ?? 0; // Web only supports integer changes... - int newZoomDelta = zoomDelta < 0 ? zoomDelta.floor() : zoomDelta.ceil(); + final int newZoomDelta = + zoomDelta < 0 ? zoomDelta.floor() : zoomDelta.ceil(); if (json.length == 3) { // With focus try { - focusLatLng = _pixelToLatLng(map, json[2][0], json[2][1]); + focusLatLng = + _pixelToLatLng(map, json[2][0] as int, json[2][1] as int); } catch (e) { // https://github.com/a14n/dart-google-maps/issues/87 // print('Error computing new focus LatLng. JS Error: ' + e.toString()); @@ -409,7 +477,7 @@ void _applyCameraUpdate(gmaps.GMap map, CameraUpdate update) { map.zoom = (map.zoom ?? 0) - 1; break; case 'zoomTo': - map.zoom = json[1]; + map.zoom = json[1] as num?; break; default: throw UnimplementedError('Unimplemented CameraMove: ${json[0]}.'); @@ -418,9 +486,9 @@ void _applyCameraUpdate(gmaps.GMap map, CameraUpdate update) { // original JS by: Byron Singh (https://stackoverflow.com/a/30541162) gmaps.LatLng _pixelToLatLng(gmaps.GMap map, int x, int y) { - final bounds = map.bounds; - final projection = map.projection; - final zoom = map.zoom; + final gmaps.LatLngBounds? bounds = map.bounds; + final gmaps.Projection? projection = map.projection; + final num? zoom = map.zoom; assert( bounds != null, 'Map Bounds required to compute LatLng of screen x/y.'); @@ -429,15 +497,15 @@ gmaps.LatLng _pixelToLatLng(gmaps.GMap map, int x, int y) { assert(zoom != null, 'Current map zoom level required to compute LatLng of screen x/y'); - final ne = bounds!.northEast; - final sw = bounds.southWest; + final gmaps.LatLng ne = bounds!.northEast; + final gmaps.LatLng sw = bounds.southWest; - final topRight = projection!.fromLatLngToPoint!(ne)!; - final bottomLeft = projection.fromLatLngToPoint!(sw)!; + final gmaps.Point topRight = projection!.fromLatLngToPoint!(ne)!; + final gmaps.Point bottomLeft = projection.fromLatLngToPoint!(sw)!; - final scale = 1 << (zoom!.toInt()); // 2 ^ zoom + final int scale = 1 << (zoom!.toInt()); // 2 ^ zoom - final point = + final gmaps.Point point = gmaps.Point((x / scale) + bottomLeft.x!, (y / scale) + topRight.y!); return projection.fromPointToLatLng!(point)!; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart index edf47764f346..a659fb218803 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart @@ -11,6 +11,40 @@ typedef DebugCreateMapFunction = gmaps.GMap Function( /// Encapsulates a [gmaps.GMap], its events, and where in the DOM it's rendered. class GoogleMapController { + /// Initializes the GMap, and the sub-controllers related to it. Wires events. + GoogleMapController({ + required int mapId, + required StreamController> streamController, + required MapWidgetConfiguration widgetConfiguration, + MapObjects mapObjects = const MapObjects(), + MapConfiguration mapConfiguration = const MapConfiguration(), + }) : _mapId = mapId, + _streamController = streamController, + _initialCameraPosition = widgetConfiguration.initialCameraPosition, + _markers = mapObjects.markers, + _polygons = mapObjects.polygons, + _polylines = mapObjects.polylines, + _circles = mapObjects.circles, + _lastMapConfiguration = mapConfiguration { + _circlesController = CirclesController(stream: _streamController); + _polygonsController = PolygonsController(stream: _streamController); + _polylinesController = PolylinesController(stream: _streamController); + _markersController = MarkersController(stream: _streamController); + + // Register the view factory that will hold the `_div` that holds the map in the DOM. + // The `_div` needs to be created outside of the ViewFactory (and cached!) so we can + // use it to create the [gmaps.GMap] in the `init()` method of this class. + _div = DivElement() + ..id = _getViewType(mapId) + ..style.width = '100%' + ..style.height = '100%'; + + ui.platformViewRegistry.registerViewFactory( + _getViewType(mapId), + (int viewId) => _div, + ); + } + // The internal ID of the map. Used to broadcast events, DOM IDs and everything where a unique ID is needed. final int _mapId; @@ -19,9 +53,10 @@ class GoogleMapController { final Set _polygons; final Set _polylines; final Set _circles; - // The raw options passed by the user, before converting to gmaps. + // The configuraiton passed by the user, before converting to gmaps. // Caching this allows us to re-create the map faithfully when needed. - Map _rawMapOptions = {}; + MapConfiguration _lastMapConfiguration = const MapConfiguration(); + List _lastStyles = const []; // Creates the 'viewType' for the _widget String _getViewType(int mapId) => 'plugins.flutter.io/google_maps_$mapId'; @@ -51,14 +86,14 @@ class GoogleMapController { gmaps.GMap? _googleMap; // The StreamController used by this controller and the geometry ones. - final StreamController _streamController; + final StreamController> _streamController; /// The StreamController for the events of this Map. Only for integration testing. @visibleForTesting - StreamController get stream => _streamController; + StreamController> get stream => _streamController; /// The Stream over which this controller broadcasts events. - Stream get events => _streamController.stream; + Stream> get events => _streamController.stream; // Geometry controllers, for different features of the map. CirclesController? _circlesController; @@ -71,46 +106,6 @@ class GoogleMapController { // Keeps track if the map is moving or not. bool _mapIsMoving = false; - /// Initializes the GMap, and the sub-controllers related to it. Wires events. - GoogleMapController({ - required int mapId, - required StreamController streamController, - required CameraPosition initialCameraPosition, - Set markers = const {}, - Set polygons = const {}, - Set polylines = const {}, - Set circles = const {}, - Set tileOverlays = const {}, - Set> gestureRecognizers = - const >{}, - Map mapOptions = const {}, - }) : _mapId = mapId, - _streamController = streamController, - _initialCameraPosition = initialCameraPosition, - _markers = markers, - _polygons = polygons, - _polylines = polylines, - _circles = circles, - _rawMapOptions = mapOptions { - _circlesController = CirclesController(stream: this._streamController); - _polygonsController = PolygonsController(stream: this._streamController); - _polylinesController = PolylinesController(stream: this._streamController); - _markersController = MarkersController(stream: this._streamController); - - // Register the view factory that will hold the `_div` that holds the map in the DOM. - // The `_div` needs to be created outside of the ViewFactory (and cached!) so we can - // use it to create the [gmaps.GMap] in the `init()` method of this class. - _div = DivElement() - ..id = _getViewType(mapId) - ..style.width = '100%' - ..style.height = '100%'; - - ui.platformViewRegistry.registerViewFactory( - _getViewType(mapId), - (int viewId) => _div, - ); - } - /// Overrides certain properties to install mocks defined during testing. @visibleForTesting void debugSetOverrides({ @@ -161,12 +156,13 @@ class GoogleMapController { /// Failure to call this method would result in the GMap not rendering at all, /// and most of the public methods on this class no-op'ing. void init() { - var options = _rawOptionsToGmapsOptions(_rawMapOptions); + gmaps.MapOptions options = _configurationAndStyleToGmapsOptions( + _lastMapConfiguration, _lastStyles); // Initial position can only to be set here! options = _applyInitialPosition(_initialCameraPosition, options); // Create the map... - final map = _createMap(_div, options); + final gmaps.GMap map = _createMap(_div, options); _googleMap = map; _attachMapEvents(map); @@ -180,28 +176,28 @@ class GoogleMapController { polylines: _polylines, ); - _setTrafficLayer(map, _isTrafficLayerEnabled(_rawMapOptions)); + _setTrafficLayer(map, _lastMapConfiguration.trafficEnabled ?? false); } // Funnels map gmap events into the plugin's stream controller. void _attachMapEvents(gmaps.GMap map) { - map.onTilesloaded.first.then((event) { + map.onTilesloaded.first.then((void _) { // Report the map as ready to go the first time the tiles load _streamController.add(WebMapReadyEvent(_mapId)); }); - map.onClick.listen((event) { + map.onClick.listen((gmaps.IconMouseEvent event) { assert(event.latLng != null); _streamController.add( MapTapEvent(_mapId, _gmLatLngToLatLng(event.latLng!)), ); }); - map.onRightclick.listen((event) { + map.onRightclick.listen((gmaps.MapMouseEvent event) { assert(event.latLng != null); _streamController.add( MapLongPressEvent(_mapId, _gmLatLngToLatLng(event.latLng!)), ); }); - map.onBoundsChanged.listen((event) { + map.onBoundsChanged.listen((void _) { if (!_mapIsMoving) { _mapIsMoving = true; _streamController.add(CameraMoveStartedEvent(_mapId)); @@ -210,7 +206,7 @@ class GoogleMapController { CameraMoveEvent(_mapId, _gmViewportToCameraPosition(map)), ); }); - map.onIdle.listen((event) { + map.onIdle.listen((void _) { _mapIsMoving = false; _streamController.add(CameraIdleEvent(_mapId)); }); @@ -243,15 +239,15 @@ class GoogleMapController { // Renders the initial sets of geometry. void _renderInitialGeometry({ - Set markers = const {}, - Set circles = const {}, - Set polygons = const {}, - Set polylines = const {}, + Set markers = const {}, + Set circles = const {}, + Set polygons = const {}, + Set polylines = const {}, }) { assert( _controllersBoundToMap, - 'Geometry controllers must be bound to a map before any geometry can ' + - 'be added to them. Ensure _attachGeometryControllers is called first.'); + 'Geometry controllers must be bound to a map before any geometry can ' + 'be added to them. Ensure _attachGeometryControllers is called first.'); // The above assert will only succeed if the controllers have been bound to a map // in the [_attachGeometryControllers] method, which ensures that all these @@ -263,30 +259,37 @@ class GoogleMapController { _polylinesController!.addPolylines(polylines); } - // Merges new options coming from the plugin into the _rawMapOptions map. + // Merges new options coming from the plugin into _lastConfiguration. // - // Returns the updated _rawMapOptions object. - Map _mergeRawOptions(Map newOptions) { - _rawMapOptions = { - ..._rawMapOptions, - ...newOptions, - }; - return _rawMapOptions; + // Returns the updated _lastConfiguration object. + MapConfiguration _mergeConfigurations(MapConfiguration update) { + _lastMapConfiguration = _lastMapConfiguration.applyDiff(update); + return _lastMapConfiguration; } - /// Updates the map options from a `Map`. + /// Updates the map options from a [MapConfiguration]. /// - /// This method converts the map into the proper [gmaps.MapOptions] - void updateRawOptions(Map optionsUpdate) { + /// This method converts the map into the proper [gmaps.MapOptions]. + void updateMapConfiguration(MapConfiguration update) { assert(_googleMap != null, 'Cannot update options on a null map.'); - final newOptions = _mergeRawOptions(optionsUpdate); + final MapConfiguration newConfiguration = _mergeConfigurations(update); + final gmaps.MapOptions newOptions = + _configurationAndStyleToGmapsOptions(newConfiguration, _lastStyles); + + _setOptions(newOptions); + _setTrafficLayer(_googleMap!, newConfiguration.trafficEnabled ?? false); + } - _setOptions(_rawOptionsToGmapsOptions(newOptions)); - _setTrafficLayer(_googleMap!, _isTrafficLayerEnabled(newOptions)); + /// Updates the map options with a new list of [styles]. + void updateStyles(List styles) { + _lastStyles = styles; + _setOptions( + _configurationAndStyleToGmapsOptions(_lastMapConfiguration, styles)); } // Sets new [gmaps.MapOptions] on the wrapped map. + // ignore: use_setters_to_change_properties void _setOptions(gmaps.MapOptions options) { _googleMap?.options = options; } @@ -309,9 +312,11 @@ class GoogleMapController { Future getVisibleRegion() async { assert(_googleMap != null, 'Cannot get the visible region of a null map.'); - return _gmLatLngBoundsTolatLngBounds( - await _googleMap!.bounds ?? _nullGmapsLatLngBounds, - ); + final gmaps.LatLngBounds bounds = + await Future.value(_googleMap!.bounds) ?? + _nullGmapsLatLngBounds; + + return _gmLatLngBoundsTolatLngBounds(bounds); } /// Returns the [ScreenCoordinate] for a given viewport [LatLng]. @@ -319,7 +324,8 @@ class GoogleMapController { assert(_googleMap != null, 'Cannot get the screen coordinates with a null map.'); - final point = toScreenLocation(_googleMap!, _latLngToGmLatLng(latLng)); + final gmaps.Point point = + toScreenLocation(_googleMap!, _latLngToGmLatLng(latLng)); return ScreenCoordinate(x: point.x!.toInt(), y: point.y!.toInt()); } @@ -424,8 +430,8 @@ class GoogleMapController { } } -/// An event fired when a [mapId] on web is interactive. -class WebMapReadyEvent extends MapEvent { +/// A MapEvent event fired when a [mapId] on web is interactive. +class WebMapReadyEvent extends MapEvent { /// Build a WebMapReady Event for the map represented by `mapId`. WebMapReadyEvent(int mapId) : super(mapId, null); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart index 47bfdc7bba15..c2085a2bddfc 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart @@ -14,23 +14,24 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { } // A cache of map controllers by map Id. - Map _mapById = Map(); + Map _mapById = {}; /// Allows tests to inject controllers without going through the buildView flow. @visibleForTesting + // ignore: use_setters_to_change_properties void debugSetMapById(Map mapById) { _mapById = mapById; } // Convenience getter for a stream of events filtered by their mapId. - Stream _events(int mapId) => _map(mapId).events; + Stream> _events(int mapId) => _map(mapId).events; // Convenience getter for a map controller by its mapId. GoogleMapController _map(int mapId) { - final controller = _mapById[mapId]; + final GoogleMapController? controller = _mapById[mapId]; assert(controller != null, 'Maps cannot be retrieved before calling buildView!'); - return controller; + return controller!; } @override @@ -46,11 +47,11 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { /// This attempts to merge the new `optionsUpdate` passed in, with the previous /// options passed to the map (in other updates, or when creating it). @override - Future updateMapOptions( - Map optionsUpdate, { + Future updateMapConfiguration( + MapConfiguration update, { required int mapId, }) async { - _map(mapId).updateRawOptions(optionsUpdate); + _map(mapId).updateMapConfiguration(update); } /// Applies the passed in `markerUpdates` to the `mapId`. @@ -134,9 +135,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { String? mapStyle, { required int mapId, }) async { - _map(mapId).updateRawOptions({ - 'styles': _mapStyles(mapStyle), - }); + _map(mapId).updateStyles(_mapStyles(mapStyle)); } /// Returns the bounds of the current viewport. @@ -288,41 +287,35 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { } @override - Widget buildView( + Widget buildViewWithConfiguration( int creationId, PlatformViewCreatedCallback onPlatformViewCreated, { - required CameraPosition initialCameraPosition, - Set markers = const {}, - Set polygons = const {}, - Set polylines = const {}, - Set circles = const {}, - Set tileOverlays = const {}, - Set>? gestureRecognizers = - const >{}, - Map mapOptions = const {}, + required MapWidgetConfiguration widgetConfiguration, + MapObjects mapObjects = const MapObjects(), + MapConfiguration mapConfiguration = const MapConfiguration(), }) { // Bail fast if we've already rendered this map ID... if (_mapById[creationId]?.widget != null) { - return _mapById[creationId].widget; + return _mapById[creationId]!.widget!; } - final StreamController controller = - StreamController.broadcast(); + final StreamController> controller = + StreamController>.broadcast(); - final mapController = GoogleMapController( - initialCameraPosition: initialCameraPosition, + final GoogleMapController mapController = GoogleMapController( mapId: creationId, streamController: controller, - markers: markers, - polygons: polygons, - polylines: polylines, - circles: circles, - mapOptions: mapOptions, + widgetConfiguration: widgetConfiguration, + mapObjects: mapObjects, + mapConfiguration: mapConfiguration, )..init(); // Initialize the controller _mapById[creationId] = mapController; - mapController.events.whereType().first.then((event) { + mapController.events + .whereType() + .first + .then((WebMapReadyEvent event) { assert(creationId == event.mapId, 'Received WebMapReadyEvent for the wrong map'); // Notify the plugin now that there's a fully initialized controller. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart index c4cd40f43323..9d607e9bbc6a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart @@ -6,14 +6,6 @@ part of google_maps_flutter_web; /// The `MarkerController` class wraps a [gmaps.Marker], how it handles events, and its associated (optional) [gmaps.InfoWindow] widget. class MarkerController { - gmaps.Marker? _marker; - - final bool _consumeTapEvents; - - final gmaps.InfoWindow? _infoWindow; - - bool _infoWindowShown = false; - /// Creates a `MarkerController`, which wraps a [gmaps.Marker] object, its `onTap`/`onDrag` behavior, and its associated [gmaps.InfoWindow]. MarkerController({ required gmaps.Marker marker, @@ -27,12 +19,12 @@ class MarkerController { _infoWindow = infoWindow, _consumeTapEvents = consumeTapEvents { if (onTap != null) { - marker.onClick.listen((event) { + marker.onClick.listen((gmaps.MapMouseEvent event) { onTap.call(); }); } if (onDragStart != null) { - marker.onDragstart.listen((event) { + marker.onDragstart.listen((gmaps.MapMouseEvent event) { if (marker != null) { marker.position = event.latLng; } @@ -40,7 +32,7 @@ class MarkerController { }); } if (onDrag != null) { - marker.onDrag.listen((event) { + marker.onDrag.listen((gmaps.MapMouseEvent event) { if (marker != null) { marker.position = event.latLng; } @@ -48,7 +40,7 @@ class MarkerController { }); } if (onDragEnd != null) { - marker.onDragend.listen((event) { + marker.onDragend.listen((gmaps.MapMouseEvent event) { if (marker != null) { marker.position = event.latLng; } @@ -57,6 +49,14 @@ class MarkerController { } } + gmaps.Marker? _marker; + + final bool _consumeTapEvents; + + final gmaps.InfoWindow? _infoWindow; + + bool _infoWindowShown = false; + /// Returns `true` if this Controller will use its own `onTap` handler to consume events. bool get consumeTapEvents => _consumeTapEvents; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart index 542a48bcb707..1a712b109677 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart @@ -6,17 +6,17 @@ part of google_maps_flutter_web; /// This class manages a set of [MarkerController]s associated to a [GoogleMapController]. class MarkersController extends GeometryController { + /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. + MarkersController({ + required StreamController> stream, + }) : _streamController = stream, + _markerIdToController = {}; + // A cache of [MarkerController]s indexed by their [MarkerId]. final Map _markerIdToController; // The stream over which markers broadcast their events - StreamController _streamController; - - /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. - MarkersController({ - required StreamController stream, - }) : _streamController = stream, - _markerIdToController = Map(); + final StreamController> _streamController; /// Returns the cache of [MarkerController]s. Test only. @visibleForTesting @@ -34,32 +34,35 @@ class MarkersController extends GeometryController { return; } - final infoWindowOptions = _infoWindowOptionsFromMarker(marker); + final gmaps.InfoWindowOptions? infoWindowOptions = + _infoWindowOptionsFromMarker(marker); gmaps.InfoWindow? gmInfoWindow; if (infoWindowOptions != null) { gmInfoWindow = gmaps.InfoWindow(infoWindowOptions); // Google Maps' JS SDK does not have a click event on the InfoWindow, so // we make one... - if (infoWindowOptions.content is HtmlElement) { - final content = infoWindowOptions.content as HtmlElement; + if (infoWindowOptions.content != null && + infoWindowOptions.content is HtmlElement) { + final HtmlElement content = infoWindowOptions.content! as HtmlElement; content.onClick.listen((_) { _onInfoWindowTap(marker.markerId); }); } } - final currentMarker = _markerIdToController[marker.markerId]?.marker; + final gmaps.Marker? currentMarker = + _markerIdToController[marker.markerId]?.marker; - final populationOptions = _markerOptionsFromMarker(marker, currentMarker); - gmaps.Marker gmMarker = gmaps.Marker(populationOptions); - gmMarker.map = googleMap; - MarkerController controller = MarkerController( + final gmaps.MarkerOptions markerOptions = + _markerOptionsFromMarker(marker, currentMarker); + final gmaps.Marker gmMarker = gmaps.Marker(markerOptions)..map = googleMap; + final MarkerController controller = MarkerController( marker: gmMarker, infoWindow: gmInfoWindow, consumeTapEvents: marker.consumeTapEvents, onTap: () { - this.showMarkerInfoWindow(marker.markerId); + showMarkerInfoWindow(marker.markerId); _onMarkerTap(marker.markerId); }, onDragStart: (gmaps.LatLng latLng) { @@ -81,13 +84,15 @@ class MarkersController extends GeometryController { } void _changeMarker(Marker marker) { - MarkerController? markerController = _markerIdToController[marker.markerId]; + final MarkerController? markerController = + _markerIdToController[marker.markerId]; if (markerController != null) { - final markerOptions = _markerOptionsFromMarker( + final gmaps.MarkerOptions markerOptions = _markerOptionsFromMarker( marker, markerController.marker, ); - final infoWindow = _infoWindowOptionsFromMarker(marker); + final gmaps.InfoWindowOptions? infoWindow = + _infoWindowOptionsFromMarker(marker); markerController.update( markerOptions, newInfoWindowContent: infoWindow?.content as HtmlElement?, @@ -113,7 +118,7 @@ class MarkersController extends GeometryController { /// See also [hideMarkerInfoWindow] and [isInfoWindowShown]. void showMarkerInfoWindow(MarkerId markerId) { _hideAllMarkerInfoWindow(); - MarkerController? markerController = _markerIdToController[markerId]; + final MarkerController? markerController = _markerIdToController[markerId]; markerController?.showInfoWindow(); } @@ -121,7 +126,7 @@ class MarkersController extends GeometryController { /// /// See also [showMarkerInfoWindow] and [isInfoWindowShown]. void hideMarkerInfoWindow(MarkerId markerId) { - MarkerController? markerController = _markerIdToController[markerId]; + final MarkerController? markerController = _markerIdToController[markerId]; markerController?.hideInfoWindow(); } @@ -129,7 +134,7 @@ class MarkersController extends GeometryController { /// /// See also [showMarkerInfoWindow] and [hideMarkerInfoWindow]. bool isInfoWindowShown(MarkerId markerId) { - MarkerController? markerController = _markerIdToController[markerId]; + final MarkerController? markerController = _markerIdToController[markerId]; return markerController?.infoWindowShown ?? false; } @@ -172,8 +177,10 @@ class MarkersController extends GeometryController { void _hideAllMarkerInfoWindow() { _markerIdToController.values - .where((controller) => - controller == null ? false : controller.infoWindowShown) - .forEach((controller) => controller.hideInfoWindow()); + .where((MarkerController? controller) => + controller?.infoWindowShown ?? false) + .forEach((MarkerController controller) { + controller.hideInfoWindow(); + }); } } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart index 9921d2ff3876..719eeeecdb43 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart @@ -6,10 +6,6 @@ part of google_maps_flutter_web; /// The `PolygonController` class wraps a [gmaps.Polygon] and its `onTap` behavior. class PolygonController { - gmaps.Polygon? _polygon; - - final bool _consumeTapEvents; - /// Creates a `PolygonController` that wraps a [gmaps.Polygon] object and its `onTap` behavior. PolygonController({ required gmaps.Polygon polygon, @@ -18,12 +14,16 @@ class PolygonController { }) : _polygon = polygon, _consumeTapEvents = consumeTapEvents { if (onTap != null) { - polygon.onClick.listen((event) { + polygon.onClick.listen((gmaps.PolyMouseEvent event) { onTap.call(); }); } } + gmaps.Polygon? _polygon; + + final bool _consumeTapEvents; + /// Returns the wrapped [gmaps.Polygon]. Only used for testing. @visibleForTesting gmaps.Polygon? get polygon => _polygon; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart index 8a9643156351..12e378cfc59c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.dart @@ -6,17 +6,17 @@ part of google_maps_flutter_web; /// This class manages a set of [PolygonController]s associated to a [GoogleMapController]. class PolygonsController extends GeometryController { + /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. + PolygonsController({ + required StreamController> stream, + }) : _streamController = stream, + _polygonIdToController = {}; + // A cache of [PolygonController]s indexed by their [PolygonId]. final Map _polygonIdToController; // The stream over which polygons broadcast events - StreamController _streamController; - - /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. - PolygonsController({ - required StreamController stream, - }) : _streamController = stream, - _polygonIdToController = Map(); + final StreamController> _streamController; /// Returns the cache of [PolygonController]s. Test only. @visibleForTesting @@ -27,9 +27,7 @@ class PolygonsController extends GeometryController { /// Wraps each Polygon into its corresponding [PolygonController]. void addPolygons(Set polygonsToAdd) { if (polygonsToAdd != null) { - polygonsToAdd.forEach((polygon) { - _addPolygon(polygon); - }); + polygonsToAdd.forEach(_addPolygon); } } @@ -38,10 +36,11 @@ class PolygonsController extends GeometryController { return; } - final populationOptions = _polygonOptionsFromPolygon(googleMap, polygon); - gmaps.Polygon gmPolygon = gmaps.Polygon(populationOptions); - gmPolygon.map = googleMap; - PolygonController controller = PolygonController( + final gmaps.PolygonOptions polygonOptions = + _polygonOptionsFromPolygon(googleMap, polygon); + final gmaps.Polygon gmPolygon = gmaps.Polygon(polygonOptions) + ..map = googleMap; + final PolygonController controller = PolygonController( polygon: gmPolygon, consumeTapEvents: polygon.consumeTapEvents, onTap: () { @@ -53,26 +52,27 @@ class PolygonsController extends GeometryController { /// Updates a set of [Polygon] objects with new options. void changePolygons(Set polygonsToChange) { if (polygonsToChange != null) { - polygonsToChange.forEach((polygonToChange) { - _changePolygon(polygonToChange); - }); + polygonsToChange.forEach(_changePolygon); } } void _changePolygon(Polygon polygon) { - PolygonController? polygonController = + final PolygonController? polygonController = _polygonIdToController[polygon.polygonId]; polygonController?.update(_polygonOptionsFromPolygon(googleMap, polygon)); } /// Removes a set of [PolygonId]s from the cache. void removePolygons(Set polygonIdsToRemove) { - polygonIdsToRemove.forEach((polygonId) { - final PolygonController? polygonController = - _polygonIdToController[polygonId]; - polygonController?.remove(); - _polygonIdToController.remove(polygonId); - }); + polygonIdsToRemove.forEach(_removePolygon); + } + + // Removes a polygon and its controller by its [PolygonId]. + void _removePolygon(PolygonId polygonId) { + final PolygonController? polygonController = + _polygonIdToController[polygonId]; + polygonController?.remove(); + _polygonIdToController.remove(polygonId); } // Handle internal events diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart index eb4b6d88b503..428bb7fce016 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.dart @@ -6,10 +6,6 @@ part of google_maps_flutter_web; /// The `PolygonController` class wraps a [gmaps.Polyline] and its `onTap` behavior. class PolylineController { - gmaps.Polyline? _polyline; - - final bool _consumeTapEvents; - /// Creates a `PolylineController` that wraps a [gmaps.Polyline] object and its `onTap` behavior. PolylineController({ required gmaps.Polyline polyline, @@ -18,12 +14,16 @@ class PolylineController { }) : _polyline = polyline, _consumeTapEvents = consumeTapEvents { if (onTap != null) { - polyline.onClick.listen((event) { + polyline.onClick.listen((gmaps.PolyMouseEvent event) { onTap.call(); }); } } + gmaps.Polyline? _polyline; + + final bool _consumeTapEvents; + /// Returns the wrapped [gmaps.Polyline]. Only used for testing. @visibleForTesting gmaps.Polyline? get line => _polyline; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart index 695b29554c04..2d3f1618b42c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.dart @@ -6,17 +6,17 @@ part of google_maps_flutter_web; /// This class manages a set of [PolylinesController]s associated to a [GoogleMapController]. class PolylinesController extends GeometryController { + /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. + PolylinesController({ + required StreamController> stream, + }) : _streamController = stream, + _polylineIdToController = {}; + // A cache of [PolylineController]s indexed by their [PolylineId]. final Map _polylineIdToController; // The stream over which polylines broadcast their events - StreamController _streamController; - - /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. - PolylinesController({ - required StreamController stream, - }) : _streamController = stream, - _polylineIdToController = Map(); + final StreamController> _streamController; /// Returns the cache of [PolylineContrller]s. Test only. @visibleForTesting @@ -26,9 +26,7 @@ class PolylinesController extends GeometryController { /// /// Wraps each line into its corresponding [PolylineController]. void addPolylines(Set polylinesToAdd) { - polylinesToAdd.forEach((polyline) { - _addPolyline(polyline); - }); + polylinesToAdd.forEach(_addPolyline); } void _addPolyline(Polyline polyline) { @@ -36,10 +34,11 @@ class PolylinesController extends GeometryController { return; } - final polylineOptions = _polylineOptionsFromPolyline(googleMap, polyline); - gmaps.Polyline gmPolyline = gmaps.Polyline(polylineOptions); - gmPolyline.map = googleMap; - PolylineController controller = PolylineController( + final gmaps.PolylineOptions polylineOptions = + _polylineOptionsFromPolyline(googleMap, polyline); + final gmaps.Polyline gmPolyline = gmaps.Polyline(polylineOptions) + ..map = googleMap; + final PolylineController controller = PolylineController( polyline: gmPolyline, consumeTapEvents: polyline.consumeTapEvents, onTap: () { @@ -50,13 +49,11 @@ class PolylinesController extends GeometryController { /// Updates a set of [Polyline] objects with new options. void changePolylines(Set polylinesToChange) { - polylinesToChange.forEach((polylineToChange) { - _changePolyline(polylineToChange); - }); + polylinesToChange.forEach(_changePolyline); } void _changePolyline(Polyline polyline) { - PolylineController? polylineController = + final PolylineController? polylineController = _polylineIdToController[polyline.polylineId]; polylineController ?.update(_polylineOptionsFromPolyline(googleMap, polyline)); @@ -64,12 +61,15 @@ class PolylinesController extends GeometryController { /// Removes a set of [PolylineId]s from the cache. void removePolylines(Set polylineIdsToRemove) { - polylineIdsToRemove.forEach((polylineId) { - final PolylineController? polylineController = - _polylineIdToController[polylineId]; - polylineController?.remove(); - _polylineIdToController.remove(polylineId); - }); + polylineIdsToRemove.forEach(_removePolyline); + } + + // Removes a polyline and its controller by its [PolylineId]. + void _removePolyline(PolylineId polylineId) { + final PolylineController? polylineController = + _polylineIdToController[polylineId]; + polylineController?.remove(); + _polylineIdToController.remove(polylineId); } // Handle internal events diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui.dart index 5eacec5fe867..2b254a95b951 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui.dart @@ -5,6 +5,6 @@ /// This file shims dart:ui in web-only scenarios, getting rid of the need to /// suppress analyzer warnings. -// TODO(flutter/flutter#55000) Remove this file once web-only dart:ui APIs +// TODO(ditman): Remove this file once web-only dart:ui APIs, https://github.com/flutter/flutter/issues/55000 // are exposed from a dedicated place. export 'dart_ui_fake.dart' if (dart.library.html) 'dart_ui_real.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui_fake.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui_fake.dart index f2862af8b704..40d8f1903111 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui_fake.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui_fake.dart @@ -7,21 +7,26 @@ import 'dart:html' as html; // Fake interface for the logic that this package needs from (web-only) dart:ui. // This is conditionally exported so the analyzer sees these methods as available. +// ignore_for_file: avoid_classes_with_only_static_members +// ignore_for_file: camel_case_types + /// Shim for web_ui engine.PlatformViewRegistry -/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62 +/// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/ui.dart#L62 class platformViewRegistry { /// Shim for registerViewFactory - /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72 - static registerViewFactory( - String viewTypeId, html.Element Function(int viewId) viewFactory) {} + /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/ui.dart#L72 + static bool registerViewFactory( + String viewTypeId, html.Element Function(int viewId) viewFactory) { + return false; + } } /// Shim for web_ui engine.AssetManager. -/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12 +/// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L12 class webOnlyAssetManager { /// Shim for getAssetUrl. - /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L45 - static getAssetUrl(String asset) {} + /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L45 + static String getAssetUrl(String asset) => ''; } /// Signature of callbacks that have no arguments and return no data. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart index 2963111fdcc3..fc25b18b43ec 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart @@ -29,9 +29,9 @@ import 'package:google_maps/google_maps.dart' as gmaps; /// /// See: https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/Projection#public-point-toscreenlocation-latlng-location gmaps.Point toScreenLocation(gmaps.GMap map, gmaps.LatLng coords) { - final zoom = map.zoom; - final bounds = map.bounds; - final projection = map.projection; + final num? zoom = map.zoom; + final gmaps.LatLngBounds? bounds = map.bounds; + final gmaps.Projection? projection = map.projection; assert( bounds != null, 'Map Bounds required to compute screen x/y of LatLng.'); @@ -40,15 +40,15 @@ gmaps.Point toScreenLocation(gmaps.GMap map, gmaps.LatLng coords) { assert(zoom != null, 'Current map zoom level required to compute screen x/y of LatLng.'); - final ne = bounds!.northEast; - final sw = bounds.southWest; + final gmaps.LatLng ne = bounds!.northEast; + final gmaps.LatLng sw = bounds.southWest; - final topRight = projection!.fromLatLngToPoint!(ne)!; - final bottomLeft = projection.fromLatLngToPoint!(sw)!; + final gmaps.Point topRight = projection!.fromLatLngToPoint!(ne)!; + final gmaps.Point bottomLeft = projection.fromLatLngToPoint!(sw)!; - final scale = 1 << (zoom!.toInt()); // 2 ^ zoom + final int scale = 1 << (zoom!.toInt()); // 2 ^ zoom - final worldPoint = projection.fromLatLngToPoint!(coords)!; + final gmaps.Point worldPoint = projection.fromLatLngToPoint!(coords)!; return gmaps.Point( ((worldPoint.x! - bottomLeft.x!) * scale).toInt(), diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart index ff980eb4c34b..d4e87799f4b3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart @@ -2,9 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; import 'package:google_maps/google_maps.dart' as gmaps; +import '../../google_maps_flutter_web.dart'; + /// A void function that handles a [gmaps.LatLng] as a parameter. /// /// Similar to [ui.VoidCallback], but specific for Marker drag events. 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 2780175d29e2..572d9110be8e 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.3.2+1 +version: 0.4.0+3 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: @@ -21,9 +21,8 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - google_maps_flutter_platform_interface: ^2.1.2 - google_maps: ^5.2.0 - pedantic: ^1.10.0 + google_maps: ^6.1.0 + google_maps_flutter_platform_interface: ^2.2.2 sanitize_html: ^2.0.0 stream_transform: ^2.0.0 diff --git a/packages/google_sign_in/google_sign_in/AUTHORS b/packages/google_sign_in/google_sign_in/AUTHORS index 493a0b4ef9c2..35d24a5ae0b5 100644 --- a/packages/google_sign_in/google_sign_in/AUTHORS +++ b/packages/google_sign_in/google_sign_in/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Twin Sun, LLC diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index a46023bfd788..93497841fbd5 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,43 @@ +## 5.4.2 + +* Updates minimum Flutter version to 2.10. +* Adds override for `GoogleSignInPlatform.initWithParams`. +* Fixes tests to recognize new default `forceCodeForRefreshToken` request attribute. + +## 5.4.1 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 5.4.0 + +* Adds support for configuring `serverClientId` through `GoogleSignIn` constructor. +* Adds support for Dart-based configuration as alternative to `GoogleService-Info.plist` for iOS. + +## 5.3.3 + +* Updates references to the obsolete master branch. + +## 5.3.2 + +* Enables mocking models by changing overridden operator == parameter type from `dynamic` to `Object`. +* Updates tests to use a mock platform instead of relying on default + method channel implementation internals. +* Removes example workaround to build for arm64 iOS simulators. + +## 5.3.1 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 5.3.0 + +* Moves Android and iOS implementations to federated packages. + +## 5.2.5 + +* Migrates from `ui.hash*` to `Object.hash*`. +* Adds OS version support information to README. + ## 5.2.4 * Internal code cleanup for stricter analysis options. diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index f3787474eeef..e467ca8541b9 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -6,6 +6,10 @@ _Note_: This plugin is still under development, and some APIs might not be available yet. [Feedback](https://github.com/flutter/flutter/issues) and [Pull Requests](https://github.com/flutter/plugins/pulls) are most welcome! +| | Android | iOS | Web | +|-------------|---------|--------|-----| +| **Support** | SDK 16+ | iOS 9+ | Any | + ## Platform integration ### Android integration @@ -61,6 +65,22 @@ 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: + +```dart +GoogleSignIn _googleSignIn = GoogleSignIn( + ... + // The OAuth client id of your app. This is required. + clientId: ..., + // If you need to authenticate to a backend server, specify its OAuth client. This is optional. + serverClientId: ..., +); +``` + +Note that step 7 is still required. + #### iOS additional requirement Note that according to @@ -122,4 +142,4 @@ Future _handleSignIn() async { ## Example Find the example wiring in the -[Google sign-in example application](https://github.com/flutter/plugins/blob/master/packages/google_sign_in/google_sign_in/example/lib/main.dart). +[Google sign-in example application](https://github.com/flutter/plugins/blob/main/packages/google_sign_in/google_sign_in/example/lib/main.dart). diff --git a/packages/google_sign_in/google_sign_in/android/settings.gradle b/packages/google_sign_in/google_sign_in/android/settings.gradle deleted file mode 100644 index d943fae5ece0..000000000000 --- a/packages/google_sign_in/google_sign_in/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'googlesignin' diff --git a/packages/google_sign_in/google_sign_in/example/README.md b/packages/google_sign_in/google_sign_in/example/README.md index 0e246e11a8be..24fdb3ec042d 100644 --- a/packages/google_sign_in/google_sign_in/example/README.md +++ b/packages/google_sign_in/google_sign_in/example/README.md @@ -1,8 +1,3 @@ # google_sign_in_example Demonstrates how to use the google_sign_in plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/google_sign_in/google_sign_in/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/google_sign_in/google_sign_in/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_sign_in/google_sign_in/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/google_sign_in/google_sign_in/example/android/build.gradle b/packages/google_sign_in/google_sign_in/example/android/build.gradle index e101ac08df55..c21bff8e0a2f 100644 --- a/packages/google_sign_in/google_sign_in/example/android/build.gradle +++ b/packages/google_sign_in/google_sign_in/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:7.0.1' } } diff --git a/packages/google_sign_in/google_sign_in/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in/example/android/gradle/wrapper/gradle-wrapper.properties index 019065d1d650..297f2fec363f 100644 --- a/packages/google_sign_in/google_sign_in/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_sign_in/google_sign_in/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/google_sign_in/google_sign_in/example/integration_test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/example/integration_test/google_sign_in_test.dart index be3cc89674a3..54e454c28f4a 100644 --- a/packages/google_sign_in/google_sign_in/example/integration_test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/example/integration_test/google_sign_in_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.9 - import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/packages/google_sign_in/google_sign_in/example/ios/Podfile b/packages/google_sign_in/google_sign_in/example/ios/Podfile index e577a3081fe8..f7d6a5e68c3a 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/Podfile +++ b/packages/google_sign_in/google_sign_in/example/ios/Podfile @@ -29,19 +29,10 @@ flutter_ios_podfile_setup target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - - pod 'OCMock','3.5' - end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) - target.build_configurations.each do |build_configuration| - # GoogleSignIn does not support arm64 simulators. - build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' - end end end diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj index 06857ed2bd59..6c698e15ba15 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj @@ -16,28 +16,8 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; C2FB9CBA01DB0A2DE5F31E12 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0263E28FA425D1CE928BDE15 /* libPods-Runner.a */; }; - C56D3B06A42F3B35C1F47A43 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 18AD6475292B9C45B529DDC9 /* libPods-RunnerTests.a */; }; - F76AC1A52666D0540040C8BC /* GoogleSignInTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC1A42666D0540040C8BC /* GoogleSignInTests.m */; }; - F76AC1B32666D0610040C8BC /* GoogleSignInUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC1B22666D0610040C8BC /* GoogleSignInUITests.m */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - F76AC1A72666D0540040C8BC /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; - F76AC1B52666D0610040C8BC /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -73,12 +53,6 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F582639B44581540871D9BB0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - F76AC1A22666D0540040C8BC /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F76AC1A42666D0540040C8BC /* GoogleSignInTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleSignInTests.m; sourceTree = ""; }; - F76AC1A62666D0540040C8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F76AC1B02666D0610040C8BC /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F76AC1B22666D0610040C8BC /* GoogleSignInUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleSignInUITests.m; sourceTree = ""; }; - F76AC1B42666D0610040C8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -90,21 +64,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC19F2666D0540040C8BC /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C56D3B06A42F3B35C1F47A43 /* libPods-RunnerTests.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F76AC1AD2666D0610040C8BC /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -135,8 +94,6 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - F76AC1A32666D0540040C8BC /* RunnerTests */, - F76AC1B12666D0610040C8BC /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, 840012C8B5EDBCF56B0E4AC1 /* Pods */, CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, @@ -147,8 +104,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - F76AC1A22666D0540040C8BC /* RunnerTests.xctest */, - F76AC1B02666D0610040C8BC /* RunnerUITests.xctest */, ); name = Products; sourceTree = ""; @@ -187,24 +142,6 @@ name = Frameworks; sourceTree = ""; }; - F76AC1A32666D0540040C8BC /* RunnerTests */ = { - isa = PBXGroup; - children = ( - F76AC1A42666D0540040C8BC /* GoogleSignInTests.m */, - F76AC1A62666D0540040C8BC /* Info.plist */, - ); - path = RunnerTests; - sourceTree = ""; - }; - F76AC1B12666D0610040C8BC /* RunnerUITests */ = { - isa = PBXGroup; - children = ( - F76AC1B22666D0610040C8BC /* GoogleSignInUITests.m */, - F76AC1B42666D0610040C8BC /* Info.plist */, - ); - path = RunnerUITests; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -230,43 +167,6 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; - F76AC1A12666D0540040C8BC /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F76AC1AB2666D0540040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 27975964E48117AA65B1D6C7 /* [CP] Check Pods Manifest.lock */, - F76AC19E2666D0540040C8BC /* Sources */, - F76AC19F2666D0540040C8BC /* Frameworks */, - F76AC1A02666D0540040C8BC /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F76AC1A82666D0540040C8BC /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = F76AC1A22666D0540040C8BC /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - F76AC1AF2666D0610040C8BC /* RunnerUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F76AC1B72666D0610040C8BC /* Build configuration list for PBXNativeTarget "RunnerUITests" */; - buildPhases = ( - F76AC1AC2666D0610040C8BC /* Sources */, - F76AC1AD2666D0610040C8BC /* Frameworks */, - F76AC1AE2666D0610040C8BC /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F76AC1B62666D0610040C8BC /* PBXTargetDependency */, - ); - name = RunnerUITests; - productName = RunnerUITests; - productReference = F76AC1B02666D0610040C8BC /* RunnerUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -279,16 +179,6 @@ 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; }; - F76AC1A12666D0540040C8BC = { - CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - F76AC1AF2666D0610040C8BC = { - CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -305,8 +195,6 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - F76AC1A12666D0540040C8BC /* RunnerTests */, - F76AC1AF2666D0610040C8BC /* RunnerUITests */, ); }; /* End PBXProject section */ @@ -324,45 +212,9 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC1A02666D0540040C8BC /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F76AC1AE2666D0610040C8BC /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 27975964E48117AA65B1D6C7 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -440,37 +292,8 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC19E2666D0540040C8BC /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F76AC1A52666D0540040C8BC /* GoogleSignInTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F76AC1AC2666D0610040C8BC /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F76AC1B32666D0610040C8BC /* GoogleSignInUITests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - F76AC1A82666D0540040C8BC /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = F76AC1A72666D0540040C8BC /* PBXContainerItemProxy */; - }; - F76AC1B62666D0610040C8BC /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = F76AC1B52666D0610040C8BC /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -603,7 +426,6 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -625,7 +447,6 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -641,60 +462,6 @@ }; name = Release; }; - F76AC1A92666D0540040C8BC /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 37E582FF620A90D0EB2C0851 /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; - INFOPLIST_FILE = RunnerTests/Info.plist; - 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"; - }; - name = Debug; - }; - F76AC1AA2666D0540040C8BC /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 45D93D4513839BFEA2AA74FE /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; - INFOPLIST_FILE = RunnerTests/Info.plist; - 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"; - }; - name = Release; - }; - F76AC1B82666D0610040C8BC /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_TARGET_NAME = Runner; - }; - name = Debug; - }; - F76AC1B92666D0610040C8BC /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_TARGET_NAME = Runner; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -716,24 +483,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F76AC1AB2666D0540040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F76AC1A92666D0540040C8BC /* Debug */, - F76AC1AA2666D0540040C8BC /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F76AC1B72666D0610040C8BC /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F76AC1B82666D0610040C8BC /* Debug */, - F76AC1B92666D0610040C8BC /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m b/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m deleted file mode 100644 index 6f8b821a5299..000000000000 --- a/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m +++ /dev/null @@ -1,491 +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 Flutter; - -@import XCTest; -@import google_sign_in; -@import google_sign_in.Test; -@import GoogleSignIn; - -// OCMock library doesn't generate a valid modulemap. -#import - -@interface FLTGoogleSignInPluginTest : XCTestCase - -@property(strong, nonatomic) NSObject *mockBinaryMessenger; -@property(strong, nonatomic) NSObject *mockPluginRegistrar; -@property(strong, nonatomic) FLTGoogleSignInPlugin *plugin; -@property(strong, nonatomic) id mockSignIn; - -@end - -@implementation FLTGoogleSignInPluginTest - -- (void)setUp { - [super setUp]; - self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); - self.mockPluginRegistrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); - - id mockSignIn = OCMClassMock([GIDSignIn class]); - self.mockSignIn = mockSignIn; - - OCMStub(self.mockPluginRegistrar.messenger).andReturn(self.mockBinaryMessenger); - self.plugin = [[FLTGoogleSignInPlugin alloc] initWithSignIn:mockSignIn]; - [FLTGoogleSignInPlugin registerWithRegistrar:self.mockPluginRegistrar]; -} - -- (void)testUnimplementedMethod { - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"bogus" - arguments:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(id result) { - XCTAssertEqualObjects(result, FlutterMethodNotImplemented); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -- (void)testSignOut { - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signOut" - arguments:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(id result) { - XCTAssertNil(result); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; - OCMVerify([self.mockSignIn signOut]); -} - -- (void)testDisconnect { - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"disconnect" - arguments:nil]; - - [self.plugin handleMethodCall:methodCall - result:^(id result){ - }]; - OCMVerify([self.mockSignIn disconnect]); -} - -- (void)testClearAuthCache { - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"clearAuthCache" - arguments:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(id result) { - XCTAssertNil(result); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -#pragma mark - Init - -- (void)testInitGamesSignInUnsupported { - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"init" - arguments:@{@"signInOption" : @"SignInOption.games"}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(FlutterError *result) { - XCTAssertEqualObjects(result.code, @"unsupported-options"); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -- (void)testInitGoogleServiceInfoPlist { - FlutterMethodCall *methodCall = [FlutterMethodCall - methodCallWithMethodName:@"init" - arguments:@{@"scopes" : @[ @"mockScope1" ], @"hostedDomain" : @"example.com"}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(id result) { - XCTAssertNil(result); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; - - id mockSignIn = self.mockSignIn; - OCMVerify([mockSignIn setScopes:@[ @"mockScope1" ]]); - OCMVerify([mockSignIn setHostedDomain:@"example.com"]); - - // Set in example app GoogleService-Info.plist. - OCMVerify([mockSignIn - setClientID:@"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"]); - OCMVerify([mockSignIn setServerClientID:@"YOUR_SERVER_CLIENT_ID"]); -} - -- (void)testInitNullDomain { - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"init" - arguments:@{@"hostedDomain" : [NSNull null]}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(id r) { - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; - OCMVerify([self.mockSignIn setHostedDomain:nil]); -} - -- (void)testInitDynamicClientId { - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"init" - arguments:@{@"clientId" : @"mockClientId"}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(id r) { - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; - OCMVerify([self.mockSignIn setClientID:@"mockClientId"]); -} - -#pragma mark - Is signed in - -- (void)testIsNotSignedIn { - OCMStub([self.mockSignIn hasPreviousSignIn]).andReturn(NO); - - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"isSignedIn" - arguments:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(NSNumber *result) { - XCTAssertFalse(result.boolValue); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -- (void)testIsSignedIn { - OCMStub([self.mockSignIn hasPreviousSignIn]).andReturn(YES); - - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"isSignedIn" - arguments:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(NSNumber *result) { - XCTAssertTrue(result.boolValue); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -#pragma mark - Sign in silently - -- (void)testSignInSilently { - OCMExpect([self.mockSignIn restorePreviousSignIn]); - - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signInSilently" - arguments:nil]; - - [self.plugin handleMethodCall:methodCall - result:^(id result){ - }]; - OCMVerifyAll(self.mockSignIn); -} - -- (void)testSignInSilentlyFailsConcurrently { - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signInSilently" - arguments:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - - OCMExpect([self.mockSignIn restorePreviousSignIn]).andDo(^(NSInvocation *invocation) { - // Simulate calling the same method while the previous one is in flight. - [self.plugin handleMethodCall:methodCall - result:^(FlutterError *result) { - XCTAssertEqualObjects(result.code, @"concurrent-requests"); - [expectation fulfill]; - }]; - }); - - [self.plugin handleMethodCall:methodCall - result:^(id result){ - }]; - - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -#pragma mark - Sign in - -- (void)testSignIn { - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" - arguments:nil]; - - [self.plugin handleMethodCall:methodCall - result:^(NSNumber *result){ - }]; - - id mockSignIn = self.mockSignIn; - OCMVerify([mockSignIn - setPresentingViewController:[OCMArg isKindOfClass:[FlutterViewController class]]]); - OCMVerify([mockSignIn signIn]); -} - -- (void)testSignInExecption { - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" - arguments:nil]; - OCMExpect([self.mockSignIn signIn]) - .andThrow([NSException exceptionWithName:@"MockName" reason:@"MockReason" userInfo:nil]); - - __block FlutterError *error; - XCTAssertThrows([self.plugin handleMethodCall:methodCall - result:^(FlutterError *result) { - error = result; - }]); - - XCTAssertEqualObjects(error.code, @"google_sign_in"); - XCTAssertEqualObjects(error.message, @"MockReason"); - XCTAssertEqualObjects(error.details, @"MockName"); -} - -#pragma mark - Get tokens - -- (void)testGetTokens { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - - id mockAuthentication = OCMClassMock([GIDAuthentication class]); - OCMStub([mockAuthentication idToken]).andReturn(@"mockIdToken"); - OCMStub([mockAuthentication accessToken]).andReturn(@"mockAccessToken"); - [[mockAuthentication stub] - getTokensWithHandler:[OCMArg invokeBlockWithArgs:mockAuthentication, [NSNull null], nil]]; - OCMStub([mockUser authentication]).andReturn(mockAuthentication); - - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" - arguments:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(NSDictionary *result) { - XCTAssertEqualObjects(result[@"idToken"], @"mockIdToken"); - XCTAssertEqualObjects(result[@"accessToken"], @"mockAccessToken"); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -- (void)testGetTokensNoAuthKeychainError { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - - id mockAuthentication = OCMClassMock([GIDAuthentication class]); - NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain - code:kGIDSignInErrorCodeHasNoAuthInKeychain - userInfo:nil]; - [[mockAuthentication stub] - getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; - OCMStub([mockUser authentication]).andReturn(mockAuthentication); - - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" - arguments:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(FlutterError *result) { - XCTAssertEqualObjects(result.code, @"sign_in_required"); - XCTAssertEqualObjects(result.message, kGIDSignInErrorDomain); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -- (void)testGetTokensCancelledError { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - - id mockAuthentication = OCMClassMock([GIDAuthentication class]); - NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain - code:kGIDSignInErrorCodeCanceled - userInfo:nil]; - [[mockAuthentication stub] - getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; - OCMStub([mockUser authentication]).andReturn(mockAuthentication); - - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" - arguments:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(FlutterError *result) { - XCTAssertEqualObjects(result.code, @"sign_in_canceled"); - XCTAssertEqualObjects(result.message, kGIDSignInErrorDomain); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -- (void)testGetTokensURLError { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - - id mockAuthentication = OCMClassMock([GIDAuthentication class]); - NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:nil]; - [[mockAuthentication stub] - getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; - OCMStub([mockUser authentication]).andReturn(mockAuthentication); - - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" - arguments:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(FlutterError *result) { - XCTAssertEqualObjects(result.code, @"network_error"); - XCTAssertEqualObjects(result.message, NSURLErrorDomain); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -- (void)testGetTokensUnknownError { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - - id mockAuthentication = OCMClassMock([GIDAuthentication class]); - NSError *error = [NSError errorWithDomain:@"BogusDomain" code:42 userInfo:nil]; - [[mockAuthentication stub] - getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; - OCMStub([mockUser authentication]).andReturn(mockAuthentication); - - FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" - arguments:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(FlutterError *result) { - XCTAssertEqualObjects(result.code, @"sign_in_failed"); - XCTAssertEqualObjects(result.message, @"BogusDomain"); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -#pragma mark - Request scopes - -- (void)testRequestScopesResultErrorIfNotSignedIn { - OCMStub([self.mockSignIn currentUser]).andReturn(nil); - - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"requestScopes" - arguments:@{@"scopes" : @[ @"mockScope1" ]}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(FlutterError *result) { - XCTAssertEqualObjects(result.code, @"sign_in_required"); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -- (void)testRequestScopesIfNoMissingScope { - // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - NSArray *requestedScopes = @[ @"mockScope1" ]; - OCMStub(mockUser.grantedScopes).andReturn(requestedScopes); - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"requestScopes" - arguments:@{@"scopes" : requestedScopes}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(NSNumber *result) { - XCTAssertTrue(result.boolValue); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -- (void)testRequestScopesRequestsIfNotGranted { - // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - NSArray *requestedScopes = @[ @"mockScope1" ]; - OCMStub(mockUser.grantedScopes).andReturn(@[]); - id mockSignIn = self.mockSignIn; - OCMStub([mockSignIn scopes]).andReturn(@[]); - - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"requestScopes" - arguments:@{@"scopes" : requestedScopes}]; - - [self.plugin handleMethodCall:methodCall - result:^(id r){ - }]; - - OCMVerify([mockSignIn setScopes:@[ @"mockScope1" ]]); - OCMVerify([mockSignIn signIn]); -} - -- (void)testRequestScopesReturnsFalseIfNotGranted { - // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - NSArray *requestedScopes = @[ @"mockScope1" ]; - OCMStub(mockUser.grantedScopes).andReturn(@[]); - - OCMStub([self.mockSignIn signIn]).andDo(^(NSInvocation *invocation) { - [((NSObject *)self.plugin) signIn:self.mockSignIn - didSignInForUser:mockUser - withError:nil]; - }); - - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"requestScopes" - arguments:@{@"scopes" : requestedScopes}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns false"]; - [self.plugin handleMethodCall:methodCall - result:^(NSNumber *result) { - XCTAssertFalse(result.boolValue); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -- (void)testRequestScopesReturnsTrueIfGranted { - // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - NSArray *requestedScopes = @[ @"mockScope1" ]; - NSMutableArray *availableScopes = [NSMutableArray new]; - OCMStub(mockUser.grantedScopes).andReturn(availableScopes); - - OCMStub([self.mockSignIn signIn]).andDo(^(NSInvocation *invocation) { - [availableScopes addObject:@"mockScope1"]; - [((NSObject *)self.plugin) signIn:self.mockSignIn - didSignInForUser:mockUser - withError:nil]; - }); - - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"requestScopes" - arguments:@{@"scopes" : requestedScopes}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(NSNumber *result) { - XCTAssertTrue(result.boolValue); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -@end diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index 9840a1e0a9f6..4c27543f5b18 100644 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -22,7 +22,7 @@ GoogleSignIn _googleSignIn = GoogleSignIn( void main() { runApp( - MaterialApp( + const MaterialApp( title: 'Google Sign In', home: SignInDemo(), ), @@ -30,6 +30,8 @@ void main() { } class SignInDemo extends StatefulWidget { + const SignInDemo({Key? key}) : super(key: key); + @override State createState() => SignInDemoState(); } @@ -125,8 +127,8 @@ class SignInDemoState extends State { const Text('Signed in successfully.'), Text(_contactText), ElevatedButton( - child: const Text('SIGN OUT'), onPressed: _handleSignOut, + child: const Text('SIGN OUT'), ), ElevatedButton( child: const Text('REFRESH'), @@ -140,8 +142,8 @@ class SignInDemoState extends State { children: [ const Text('You are not currently signed in.'), ElevatedButton( - child: const Text('SIGN IN'), onPressed: _handleSignIn, + child: const Text('SIGN IN'), ), ], ); 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 af9ed877e523..fbf8f7cf0591 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.5.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -19,9 +19,11 @@ dependencies: http: ^0.13.0 dev_dependencies: - espresso: ^0.1.0+2 + espresso: ^0.2.0 flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/google_sign_in/google_sign_in/example/test_driver/integration_test.dart b/packages/google_sign_in/google_sign_in/example/test_driver/integration_test.dart index 6a0e6fa82dbe..4f10f2a522f3 100644 --- a/packages/google_sign_in/google_sign_in/example/test_driver/integration_test.dart +++ b/packages/google_sign_in/google_sign_in/example/test_driver/integration_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart=2.9 - import 'package:integration_test/integration_test_driver.dart'; Future main() => integrationDriver(); diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m deleted file mode 100644 index d13d64d2ba04..000000000000 --- a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m +++ /dev/null @@ -1,304 +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 "FLTGoogleSignInPlugin.h" -#import "FLTGoogleSignInPlugin_Test.h" - -#import - -// The key within `GoogleService-Info.plist` used to hold the application's -// client id. See https://developers.google.com/identity/sign-in/ios/start -// for more info. -static NSString *const kClientIdKey = @"CLIENT_ID"; - -static NSString *const kServerClientIdKey = @"SERVER_CLIENT_ID"; - -// These error codes must match with ones declared on Android and Dart sides. -static NSString *const kErrorReasonSignInRequired = @"sign_in_required"; -static NSString *const kErrorReasonSignInCanceled = @"sign_in_canceled"; -static NSString *const kErrorReasonNetworkError = @"network_error"; -static NSString *const kErrorReasonSignInFailed = @"sign_in_failed"; - -static FlutterError *getFlutterError(NSError *error) { - NSString *errorCode; - if (error.code == kGIDSignInErrorCodeHasNoAuthInKeychain) { - errorCode = kErrorReasonSignInRequired; - } else if (error.code == kGIDSignInErrorCodeCanceled) { - errorCode = kErrorReasonSignInCanceled; - } else if ([error.domain isEqualToString:NSURLErrorDomain]) { - errorCode = kErrorReasonNetworkError; - } else { - errorCode = kErrorReasonSignInFailed; - } - return [FlutterError errorWithCode:errorCode - message:error.domain - details:error.localizedDescription]; -} - -@interface FLTGoogleSignInPlugin () -@property(strong, readonly) GIDSignIn *signIn; - -// Redeclared as not a designated initializer. -- (instancetype)init; -@end - -@implementation FLTGoogleSignInPlugin { - FlutterResult _accountRequest; - NSArray *_additionalScopesRequest; -} - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/google_sign_in" - binaryMessenger:[registrar messenger]]; - FLTGoogleSignInPlugin *instance = [[FLTGoogleSignInPlugin alloc] init]; - [registrar addApplicationDelegate:instance]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (instancetype)init { - return [self initWithSignIn:GIDSignIn.sharedInstance]; -} - -- (instancetype)initWithSignIn:(GIDSignIn *)signIn { - self = [super init]; - if (self) { - _signIn = signIn; - _signIn.delegate = self; - - // On the iOS simulator, we get "Broken pipe" errors after sign-in for some - // unknown reason. We can avoid crashing the app by ignoring them. - signal(SIGPIPE, SIG_IGN); - } - return self; -} - -#pragma mark - protocol - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([call.method isEqualToString:@"init"]) { - NSString *signInOption = call.arguments[@"signInOption"]; - if ([signInOption isEqualToString:@"SignInOption.games"]) { - result([FlutterError errorWithCode:@"unsupported-options" - message:@"Games sign in is not supported on iOS" - details:nil]); - } else { - NSString *path = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" - ofType:@"plist"]; - if (path) { - NSMutableDictionary *plist = - [[NSMutableDictionary alloc] initWithContentsOfFile:path]; - BOOL hasDynamicClientId = [call.arguments[@"clientId"] isKindOfClass:[NSString class]]; - - if (hasDynamicClientId) { - self.signIn.clientID = call.arguments[@"clientId"]; - } else { - self.signIn.clientID = plist[kClientIdKey]; - } - - self.signIn.serverClientID = plist[kServerClientIdKey]; - self.signIn.scopes = call.arguments[@"scopes"]; - if (call.arguments[@"hostedDomain"] == [NSNull null]) { - self.signIn.hostedDomain = nil; - } else { - self.signIn.hostedDomain = call.arguments[@"hostedDomain"]; - } - result(nil); - } else { - result([FlutterError errorWithCode:@"missing-config" - message:@"GoogleService-Info.plist file not found" - details:nil]); - } - } - } else if ([call.method isEqualToString:@"signInSilently"]) { - if ([self setAccountRequest:result]) { - [self.signIn restorePreviousSignIn]; - } - } else if ([call.method isEqualToString:@"isSignedIn"]) { - result(@([self.signIn hasPreviousSignIn])); - } else if ([call.method isEqualToString:@"signIn"]) { - self.signIn.presentingViewController = [self topViewController]; - - if ([self setAccountRequest:result]) { - @try { - [self.signIn signIn]; - } @catch (NSException *e) { - result([FlutterError errorWithCode:@"google_sign_in" message:e.reason details:e.name]); - [e raise]; - } - } - } else if ([call.method isEqualToString:@"getTokens"]) { - GIDGoogleUser *currentUser = self.signIn.currentUser; - GIDAuthentication *auth = currentUser.authentication; - [auth getTokensWithHandler:^void(GIDAuthentication *authentication, NSError *error) { - result(error != nil ? getFlutterError(error) : @{ - @"idToken" : authentication.idToken, - @"accessToken" : authentication.accessToken, - }); - }]; - } else if ([call.method isEqualToString:@"signOut"]) { - [self.signIn signOut]; - result(nil); - } else if ([call.method isEqualToString:@"disconnect"]) { - if ([self setAccountRequest:result]) { - [self.signIn disconnect]; - } - } else if ([call.method isEqualToString:@"clearAuthCache"]) { - // There's nothing to be done here on iOS since the expired/invalid - // tokens are refreshed automatically by getTokensWithHandler. - result(nil); - } else if ([call.method isEqualToString:@"requestScopes"]) { - GIDGoogleUser *user = self.signIn.currentUser; - if (user == nil) { - result([FlutterError errorWithCode:@"sign_in_required" - message:@"No account to grant scopes." - details:nil]); - return; - } - - NSArray *currentScopes = self.signIn.scopes; - NSArray *scopes = call.arguments[@"scopes"]; - NSArray *missingScopes = [scopes - filteredArrayUsingPredicate:[NSPredicate - predicateWithBlock:^BOOL(id scope, NSDictionary *bindings) { - return ![user.grantedScopes containsObject:scope]; - }]]; - - if (!missingScopes || !missingScopes.count) { - result(@(YES)); - return; - } - - if ([self setAccountRequest:result]) { - _additionalScopesRequest = missingScopes; - self.signIn.scopes = [currentScopes arrayByAddingObjectsFromArray:missingScopes]; - self.signIn.presentingViewController = [self topViewController]; - self.signIn.loginHint = user.profile.email; - @try { - [self.signIn signIn]; - } @catch (NSException *e) { - result([FlutterError errorWithCode:@"request_scopes" message:e.reason details:e.name]); - } - } - } else { - result(FlutterMethodNotImplemented); - } -} - -- (BOOL)setAccountRequest:(FlutterResult)request { - if (_accountRequest != nil) { - request([FlutterError errorWithCode:@"concurrent-requests" - message:@"Concurrent requests to account signin" - details:nil]); - return NO; - } - _accountRequest = request; - return YES; -} - -- (BOOL)application:(UIApplication *)app - openURL:(NSURL *)url - options:(NSDictionary *)options { - return [self.signIn handleURL:url]; -} - -#pragma mark - protocol - -- (void)signIn:(GIDSignIn *)signIn presentViewController:(UIViewController *)viewController { - UIViewController *rootViewController = - [UIApplication sharedApplication].delegate.window.rootViewController; - [rootViewController presentViewController:viewController animated:YES completion:nil]; -} - -- (void)signIn:(GIDSignIn *)signIn dismissViewController:(UIViewController *)viewController { - [viewController dismissViewControllerAnimated:YES completion:nil]; -} - -#pragma mark - protocol - -- (void)signIn:(GIDSignIn *)signIn - didSignInForUser:(GIDGoogleUser *)user - withError:(NSError *)error { - if (error != nil) { - // Forward all errors and let Dart side decide how to handle. - [self respondWithAccount:nil error:error]; - } else { - if (_additionalScopesRequest) { - bool granted = YES; - for (NSString *scope in _additionalScopesRequest) { - if (![user.grantedScopes containsObject:scope]) { - granted = NO; - break; - } - } - _accountRequest(@(granted)); - _accountRequest = nil; - _additionalScopesRequest = nil; - return; - } else { - NSURL *photoUrl; - if (user.profile.hasImage) { - // Placeholder that will be replaced by on the Dart side based on screen - // size - photoUrl = [user.profile imageURLWithDimension:1337]; - } - [self respondWithAccount:@{ - @"displayName" : user.profile.name ?: [NSNull null], - @"email" : user.profile.email ?: [NSNull null], - @"id" : user.userID ?: [NSNull null], - @"photoUrl" : [photoUrl absoluteString] ?: [NSNull null], - @"serverAuthCode" : user.serverAuthCode ?: [NSNull null] - } - error:nil]; - } - } -} - -- (void)signIn:(GIDSignIn *)signIn - didDisconnectWithUser:(GIDGoogleUser *)user - withError:(NSError *)error { - [self respondWithAccount:@{} error:nil]; -} - -#pragma mark - private methods - -- (void)respondWithAccount:(NSDictionary *)account error:(NSError *)error { - FlutterResult result = _accountRequest; - _accountRequest = nil; - result(error != nil ? getFlutterError(error) : account); -} - -- (UIViewController *)topViewController { - return [self topViewControllerFromViewController:[UIApplication sharedApplication] - .keyWindow.rootViewController]; -} - -/** - * This method recursively iterate through the view hierarchy - * to return the top most view controller. - * - * It supports the following scenarios: - * - * - The view controller is presenting another view. - * - The view controller is a UINavigationController. - * - The view controller is a UITabBarController. - * - * @return The top most view controller. - */ -- (UIViewController *)topViewControllerFromViewController:(UIViewController *)viewController { - if ([viewController isKindOfClass:[UINavigationController class]]) { - UINavigationController *navigationController = (UINavigationController *)viewController; - return [self - topViewControllerFromViewController:[navigationController.viewControllers lastObject]]; - } - if ([viewController isKindOfClass:[UITabBarController class]]) { - UITabBarController *tabController = (UITabBarController *)viewController; - return [self topViewControllerFromViewController:tabController.selectedViewController]; - } - if (viewController.presentedViewController) { - return [self topViewControllerFromViewController:viewController.presentedViewController]; - } - return viewController; -} -@end diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin_Test.h b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin_Test.h deleted file mode 100644 index 8fa6cf348018..000000000000 --- a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin_Test.h +++ /dev/null @@ -1,17 +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 header is available in the Test module. Import via "@import google_sign_in.Test;" - -#import - -@class GIDSignIn; - -/// Methods exposed for unit testing. -@interface FLTGoogleSignInPlugin () - -/// Inject @c GIDSignIn for testing. -- (instancetype)initWithSignIn:(GIDSignIn *)signIn NS_DESIGNATED_INITIALIZER; - -@end diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index 6afd409807fa..3ae022306fe6 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui' show hashValues; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show PlatformException; @@ -13,6 +12,7 @@ import 'src/common.dart'; export 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart' show SignInOption; + export 'src/common.dart'; export 'widgets.dart'; @@ -130,7 +130,7 @@ class GoogleSignInAccount implements GoogleIdentity { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (identical(this, other)) { return true; } @@ -148,7 +148,7 @@ class GoogleSignInAccount implements GoogleIdentity { @override int get hashCode => - hashValues(displayName, email, id, photoUrl, _idToken, serverAuthCode); + Object.hash(displayName, email, id, photoUrl, _idToken, serverAuthCode); @override String toString() { @@ -179,11 +179,16 @@ class GoogleSignIn { /// The [hostedDomain] argument specifies a hosted domain restriction. By /// setting this, sign in will be restricted to accounts of the user in the /// specified domain. By default, the list of accounts will not be restricted. + /// + /// The [forceCodeForRefreshToken] is used on Android to ensure the authentication + /// code can be exchanged for a refresh token after the first request. GoogleSignIn({ this.signInOption = SignInOption.standard, this.scopes = const [], this.hostedDomain, this.clientId, + this.serverClientId, + this.forceCodeForRefreshToken = false, }); /// Factory for creating default sign in user experience. @@ -191,10 +196,7 @@ class GoogleSignIn { List scopes = const [], String? hostedDomain, }) { - return GoogleSignIn( - signInOption: SignInOption.standard, - scopes: scopes, - hostedDomain: hostedDomain); + return GoogleSignIn(scopes: scopes, hostedDomain: hostedDomain); } /// Factory for creating sign in suitable for games. This option is only @@ -229,9 +231,32 @@ class GoogleSignIn { /// Domain to restrict sign-in to. final String? hostedDomain; - /// Client ID being used to connect to google sign-in. Only supported on web. + /// Client ID being used to connect to google sign-in. + /// + /// This option is not supported on all platforms (e.g. Android). It is + /// optional if file-based configuration is used. + /// + /// The value specified here has precedence over a value from a configuration + /// file. final String? clientId; + /// Client ID of the backend server to which the app needs to authenticate + /// itself. + /// + /// Optional and not supported on all platforms (e.g. web). By default, it + /// is initialized from a configuration file if available. + /// + /// The value specified here has precedence over a value from a configuration + /// file. + /// + /// [GoogleSignInAuthentication.idToken] and + /// [GoogleSignInAccount.serverAuthCode] will be specific to the backend + /// server. + final String? serverClientId; + + /// Force the authorization code to be valid for a refresh token every time. Only needed on Android. + final bool forceCodeForRefreshToken; + final StreamController _currentUserController = StreamController.broadcast(); @@ -261,15 +286,19 @@ class GoogleSignIn { } Future _ensureInitialized() { - return _initialization ??= GoogleSignInPlatform.instance.init( + return _initialization ??= + GoogleSignInPlatform.instance.initWithParams(SignInInitParameters( signInOption: signInOption, scopes: scopes, hostedDomain: hostedDomain, clientId: clientId, - )..catchError((dynamic _) { - // Invalidate initialization if it errors out. - _initialization = null; - }); + serverClientId: serverClientId, + forceCodeForRefreshToken: forceCodeForRefreshToken, + )) + ..catchError((dynamic _) { + // Invalidate initialization if it errors out. + _initialization = null; + }); } /// The most recently scheduled method call. diff --git a/packages/google_sign_in/google_sign_in/lib/widgets.dart b/packages/google_sign_in/google_sign_in/lib/widgets.dart index 61f89133fba4..f7ae5f9a6e5f 100644 --- a/packages/google_sign_in/google_sign_in/lib/widgets.dart +++ b/packages/google_sign_in/google_sign_in/lib/widgets.dart @@ -22,11 +22,13 @@ class GoogleUserCircleAvatar extends StatelessWidget { /// in place of a profile photo, or a default profile photo if the user's /// identity does not specify a `displayName`. const GoogleUserCircleAvatar({ + Key? key, required this.identity, this.placeholderPhotoUrl, this.foregroundColor, this.backgroundColor, - }) : assert(identity != null); + }) : assert(identity != null), + super(key: key); /// A regular expression that matches against the "size directive" path /// segment of Google profile image URLs. diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index de15d0bb0740..c32dee78468b 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -3,30 +3,33 @@ 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.2.4 +version: 5.4.2 + environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: platforms: android: - package: io.flutter.plugins.googlesignin - pluginClass: GoogleSignInPlugin + default_package: google_sign_in_android ios: - pluginClass: FLTGoogleSignInPlugin + default_package: google_sign_in_ios web: default_package: google_sign_in_web dependencies: flutter: sdk: flutter - google_sign_in_platform_interface: ^2.1.0 + 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 dev_dependencies: + build_runner: ^2.1.10 flutter_driver: sdk: flutter flutter_test: @@ -34,6 +37,7 @@ dev_dependencies: http: ^0.13.0 integration_test: sdk: flutter + mockito: ^5.1.0 # The example deliberately includes limited-use secrets. false_secrets: diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index 119ee50a383b..b8a596b02065 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -4,228 +4,201 @@ import 'dart:async'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in/google_sign_in.dart'; -import 'package:google_sign_in/testing.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'google_sign_in_test.mocks.dart'; + +/// Verify that [GoogleSignInAccount] can be mocked even though it's unused +// ignore: must_be_immutable +class MockGoogleSignInAccount extends Mock implements GoogleSignInAccount {} + +@GenerateMocks([GoogleSignInPlatform]) void main() { - TestWidgetsFlutterBinding.ensureInitialized(); + late MockGoogleSignInPlatform mockPlatform; group('GoogleSignIn', () { - const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/google_sign_in', - ); - - const Map kUserData = { - 'email': 'john.doe@gmail.com', - 'id': '8162538176523816253123', - 'photoUrl': 'https://lh5.googleusercontent.com/photo.jpg', - 'displayName': 'John Doe', - 'serverAuthCode': '789' - }; - - const Map kDefaultResponses = { - 'init': null, - 'signInSilently': kUserData, - 'signIn': kUserData, - 'signOut': null, - 'disconnect': null, - 'isSignedIn': true, - 'requestScopes': true, - 'getTokens': { - 'idToken': '123', - 'accessToken': '456', - 'serverAuthCode': '789', - }, - }; - - final List log = []; - late Map responses; - late GoogleSignIn googleSignIn; + final GoogleSignInUserData kDefaultUser = GoogleSignInUserData( + email: 'john.doe@gmail.com', + id: '8162538176523816253123', + photoUrl: 'https://lh5.googleusercontent.com/photo.jpg', + displayName: 'John Doe', + serverAuthCode: '789'); 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); - }); - googleSignIn = GoogleSignIn(); - log.clear(); + mockPlatform = MockGoogleSignInPlatform(); + when(mockPlatform.isMock).thenReturn(true); + when(mockPlatform.signInSilently()) + .thenAnswer((Invocation _) async => kDefaultUser); + when(mockPlatform.signIn()) + .thenAnswer((Invocation _) async => kDefaultUser); + + GoogleSignInPlatform.instance = mockPlatform; }); test('signInSilently', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + await googleSignIn.signInSilently(); + expect(googleSignIn.currentUser, isNotNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()); }); test('signIn', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + await googleSignIn.signIn(); + expect(googleSignIn.currentUser, isNotNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signIn', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.signIn()); }); - test('signIn prioritize clientId parameter when available', () async { + test('clientId parameter is forwarded to implementation', () async { const String fakeClientId = 'fakeClientId'; - googleSignIn = GoogleSignIn(clientId: fakeClientId); + final GoogleSignIn googleSignIn = GoogleSignIn(clientId: fakeClientId); + await googleSignIn.signIn(); - expect(googleSignIn.currentUser, isNotNull); - expect( - log, - [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - 'clientId': fakeClientId, - }), - isMethodCall('signIn', arguments: null), - ], - ); + + _verifyInit(mockPlatform, clientId: fakeClientId); + verify(mockPlatform.signIn()); + }); + + test('serverClientId parameter is forwarded to implementation', () async { + const String fakeServerClientId = 'fakeServerClientId'; + final GoogleSignIn googleSignIn = + GoogleSignIn(serverClientId: fakeServerClientId); + + await googleSignIn.signIn(); + + _verifyInit(mockPlatform, serverClientId: fakeServerClientId); + verify(mockPlatform.signIn()); + }); + + test('forceCodeForRefreshToken sent with init method call', () async { + final GoogleSignIn googleSignIn = + GoogleSignIn(forceCodeForRefreshToken: true); + + await googleSignIn.signIn(); + + _verifyInit(mockPlatform, forceCodeForRefreshToken: true); + verify(mockPlatform.signIn()); }); test('signOut', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + await googleSignIn.signOut(); - expect(googleSignIn.currentUser, isNull); - expect(log, [ - _isSignInMethodCall(), - isMethodCall('signOut', arguments: null), - ]); + + _verifyInit(mockPlatform); + verify(mockPlatform.signOut()); }); test('disconnect; null response', () async { - await googleSignIn.disconnect(); - expect(googleSignIn.currentUser, isNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('disconnect', arguments: null), - ], - ); - }); + final GoogleSignIn googleSignIn = GoogleSignIn(); - test('disconnect; empty response as on iOS', () async { - responses['disconnect'] = {}; await googleSignIn.disconnect(); + expect(googleSignIn.currentUser, isNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('disconnect', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.disconnect()); }); test('isSignedIn', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + when(mockPlatform.isSignedIn()).thenAnswer((Invocation _) async => true); + final bool result = await googleSignIn.isSignedIn(); + expect(result, isTrue); - expect(log, [ - _isSignInMethodCall(), - isMethodCall('isSignedIn', arguments: null), - ]); + _verifyInit(mockPlatform); + verify(mockPlatform.isSignedIn()); }); test('signIn works even if a previous call throws error in other zone', () async { - responses['signInSilently'] = Exception('Not a user'); + final GoogleSignIn googleSignIn = GoogleSignIn(); + + when(mockPlatform.signInSilently()).thenThrow(Exception('Not a user')); await runZonedGuarded(() async { expect(await googleSignIn.signInSilently(), isNull); }, (Object e, StackTrace st) {}); expect(await googleSignIn.signIn(), isNotNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - isMethodCall('signIn', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()); + verify(mockPlatform.signIn()); }); test('concurrent calls of the same method trigger sign in once', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); final List> futures = >[ googleSignIn.signInSilently(), googleSignIn.signInSilently(), ]; + expect(futures.first, isNot(futures.last), reason: 'Must return new Future'); + final List users = await Future.wait(futures); + expect(googleSignIn.currentUser, isNotNull); expect(users, [ googleSignIn.currentUser, googleSignIn.currentUser ]); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()).called(1); }); test('can sign in after previously failed attempt', () async { - responses['signInSilently'] = Exception('Not a user'); + final GoogleSignIn googleSignIn = GoogleSignIn(); + when(mockPlatform.signInSilently()).thenThrow(Exception('Not a user')); + expect(await googleSignIn.signInSilently(), isNull); expect(await googleSignIn.signIn(), isNotNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - isMethodCall('signIn', arguments: null), - ], - ); + + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()); + verify(mockPlatform.signIn()); }); test('concurrent calls of different signIn methods', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); final List> futures = >[ googleSignIn.signInSilently(), googleSignIn.signIn(), ]; expect(futures.first, isNot(futures.last)); + final List users = await Future.wait(futures); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - ], - ); + expect(users.first, users.last, reason: 'Must return the same user'); expect(googleSignIn.currentUser, users.last); + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()); + verifyNever(mockPlatform.signIn()); }); test('can sign in after aborted flow', () async { - responses['signIn'] = null; + final GoogleSignIn googleSignIn = GoogleSignIn(); + + when(mockPlatform.signIn()).thenAnswer((Invocation _) async => null); expect(await googleSignIn.signIn(), isNull); - responses['signIn'] = kUserData; + + when(mockPlatform.signIn()) + .thenAnswer((Invocation _) async => kDefaultUser); expect(await googleSignIn.signIn(), isNotNull); }); test('signOut/disconnect methods always trigger native calls', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); final List> futures = >[ googleSignIn.signOut(), @@ -233,20 +206,16 @@ void main() { googleSignIn.disconnect(), googleSignIn.disconnect(), ]; + await Future.wait(futures); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signOut', arguments: null), - isMethodCall('signOut', arguments: null), - isMethodCall('disconnect', arguments: null), - isMethodCall('disconnect', arguments: null), - ], - ); + + _verifyInit(mockPlatform); + verify(mockPlatform.signOut()).called(2); + verify(mockPlatform.disconnect()).called(2); }); test('queue of many concurrent calls', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); final List> futures = >[ googleSignIn.signInSilently(), @@ -254,182 +223,179 @@ void main() { googleSignIn.signIn(), googleSignIn.disconnect(), ]; + await Future.wait(futures); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - isMethodCall('signOut', arguments: null), - isMethodCall('signIn', arguments: null), - isMethodCall('disconnect', arguments: null), - ], - ); + + _verifyInit(mockPlatform); + verifyInOrder([ + mockPlatform.signInSilently(), + mockPlatform.signOut(), + mockPlatform.signIn(), + mockPlatform.disconnect(), + ]); }); test('signInSilently suppresses errors by default', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) { - throw 'I am an error'; - }); + final GoogleSignIn googleSignIn = GoogleSignIn(); + when(mockPlatform.signInSilently()).thenThrow(Exception('I am an error')); expect(await googleSignIn.signInSilently(), isNull); // should not throw }); - test('signInSilently forwards errors', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) { - throw 'I am an error'; - }); + test('signInSilently forwards exceptions', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + when(mockPlatform.signInSilently()).thenThrow(Exception('I am an error')); expect(googleSignIn.signInSilently(suppressErrors: false), - throwsA(isInstanceOf())); + throwsA(isInstanceOf())); }); test('signInSilently allows re-authentication to be requested', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); await googleSignIn.signInSilently(); expect(googleSignIn.currentUser, isNotNull); await googleSignIn.signInSilently(reAuthenticate: true); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - isMethodCall('signInSilently', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()).called(2); }); test('can sign in after init failed before', () async { - int initCount = 0; - channel.setMockMethodCallHandler((MethodCall methodCall) { - if (methodCall.method == 'init') { - initCount++; - if (initCount == 1) { - throw 'First init fails'; - } - } - return Future.value(responses[methodCall.method]); - }); - expect(googleSignIn.signIn(), throwsA(isInstanceOf())); + final GoogleSignIn googleSignIn = GoogleSignIn(); + + when(mockPlatform.initWithParams(any)) + .thenThrow(Exception('First init fails')); + expect(googleSignIn.signIn(), throwsA(isInstanceOf())); + + when(mockPlatform.initWithParams(any)) + .thenAnswer((Invocation _) async {}); expect(await googleSignIn.signIn(), isNotNull); }); test('created with standard factory uses correct options', () async { - googleSignIn = GoogleSignIn.standard(); + final GoogleSignIn googleSignIn = GoogleSignIn.standard(); await googleSignIn.signInSilently(); expect(googleSignIn.currentUser, isNotNull); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signInSilently', arguments: null), - ], - ); + _verifyInit(mockPlatform); + verify(mockPlatform.signInSilently()); }); test('created with defaultGamesSignIn factory uses correct options', () async { - googleSignIn = GoogleSignIn.games(); + final GoogleSignIn googleSignIn = GoogleSignIn.games(); await googleSignIn.signInSilently(); expect(googleSignIn.currentUser, isNotNull); - expect( - log, - [ - _isSignInMethodCall(signInOption: 'SignInOption.games'), - isMethodCall('signInSilently', arguments: null), - ], - ); + _verifyInit(mockPlatform, signInOption: SignInOption.games); + verify(mockPlatform.signInSilently()); }); test('authentication', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + when(mockPlatform.getTokens( + email: anyNamed('email'), + shouldRecoverAuth: anyNamed('shouldRecoverAuth'))) + .thenAnswer((Invocation _) async => GoogleSignInTokenData( + idToken: '123', + accessToken: '456', + serverAuthCode: '789', + )); + await googleSignIn.signIn(); - log.clear(); final GoogleSignInAccount user = googleSignIn.currentUser!; final GoogleSignInAuthentication auth = await user.authentication; expect(auth.accessToken, '456'); expect(auth.idToken, '123'); - expect( - log, - [ - isMethodCall('getTokens', arguments: { - 'email': 'john.doe@gmail.com', - 'shouldRecoverAuth': true, - }), - ], - ); + verify(mockPlatform.getTokens( + email: 'john.doe@gmail.com', shouldRecoverAuth: true)); }); test('requestScopes returns true once new scope is granted', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); + when(mockPlatform.requestScopes(any)) + .thenAnswer((Invocation _) async => true); + await googleSignIn.signIn(); final bool result = await googleSignIn.requestScopes(['testScope']); expect(result, isTrue); - expect( - log, - [ - _isSignInMethodCall(), - isMethodCall('signIn', arguments: null), - isMethodCall('requestScopes', arguments: { - 'scopes': ['testScope'], - }), - ], - ); - }); - }); - - group('GoogleSignIn with fake backend', () { - const FakeUser kUserData = FakeUser( - id: '8162538176523816253123', - displayName: 'John Doe', - email: 'john.doe@gmail.com', - photoUrl: 'https://lh5.googleusercontent.com/photo.jpg', - serverAuthCode: '789'); - - late GoogleSignIn googleSignIn; - - setUp(() { - final MethodChannelGoogleSignIn platformInstance = - GoogleSignInPlatform.instance as MethodChannelGoogleSignIn; - platformInstance.channel.setMockMethodCallHandler( - (FakeSignInBackend()..user = kUserData).handleMethodCall); - googleSignIn = GoogleSignIn(); + _verifyInit(mockPlatform); + verify(mockPlatform.signIn()); + verify(mockPlatform.requestScopes(['testScope'])); }); test('user starts as null', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); expect(googleSignIn.currentUser, isNull); }); test('can sign in and sign out', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); await googleSignIn.signIn(); final GoogleSignInAccount user = googleSignIn.currentUser!; - expect(user.displayName, equals(kUserData.displayName)); - expect(user.email, equals(kUserData.email)); - expect(user.id, equals(kUserData.id)); - expect(user.photoUrl, equals(kUserData.photoUrl)); - expect(user.serverAuthCode, equals(kUserData.serverAuthCode)); + expect(user.displayName, equals(kDefaultUser.displayName)); + expect(user.email, equals(kDefaultUser.email)); + expect(user.id, equals(kDefaultUser.id)); + expect(user.photoUrl, equals(kDefaultUser.photoUrl)); + expect(user.serverAuthCode, equals(kDefaultUser.serverAuthCode)); await googleSignIn.disconnect(); expect(googleSignIn.currentUser, isNull); }); test('disconnect when signout already succeeds', () async { + final GoogleSignIn googleSignIn = GoogleSignIn(); await googleSignIn.disconnect(); expect(googleSignIn.currentUser, isNull); }); }); } -Matcher _isSignInMethodCall({String signInOption = 'SignInOption.standard'}) { - return isMethodCall('init', arguments: { - 'signInOption': signInOption, - 'scopes': [], - 'hostedDomain': null, - 'clientId': null, - }); +void _verifyInit( + MockGoogleSignInPlatform mockSignIn, { + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String? hostedDomain, + String? clientId, + String? serverClientId, + bool forceCodeForRefreshToken = false, +}) { + verify(mockSignIn.initWithParams(argThat( + isA() + .having( + (SignInInitParameters p) => p.scopes, + 'scopes', + scopes, + ) + .having( + (SignInInitParameters p) => p.signInOption, + 'signInOption', + signInOption, + ) + .having( + (SignInInitParameters p) => p.hostedDomain, + 'hostedDomain', + hostedDomain, + ) + .having( + (SignInInitParameters p) => p.clientId, + 'clientId', + clientId, + ) + .having( + (SignInInitParameters p) => p.serverClientId, + 'serverClientId', + serverClientId, + ) + .having( + (SignInInitParameters p) => p.forceCodeForRefreshToken, + 'forceCodeForRefreshToken', + forceCodeForRefreshToken, + ), + ))); } diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart new file mode 100644 index 000000000000..4e669628391c --- /dev/null +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart @@ -0,0 +1,100 @@ +// Mocks generated by Mockito 5.1.0 from annotations +// in google_sign_in/test/google_sign_in_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i4; + +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart' + as _i3; +import 'package:google_sign_in_platform_interface/src/types.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +class _FakeGoogleSignInTokenData_0 extends _i1.Fake + implements _i2.GoogleSignInTokenData {} + +/// A class which mocks [GoogleSignInPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGoogleSignInPlatform extends _i1.Mock + implements _i3.GoogleSignInPlatform { + MockGoogleSignInPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + bool get isMock => + (super.noSuchMethod(Invocation.getter(#isMock), returnValue: false) + as bool); + @override + _i4.Future init( + {List? scopes = const [], + _i2.SignInOption? signInOption = _i2.SignInOption.standard, + String? hostedDomain, + String? clientId}) => + (super.noSuchMethod( + Invocation.method(#init, [], { + #scopes: scopes, + #signInOption: signInOption, + #hostedDomain: hostedDomain, + #clientId: clientId + }), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future initWithParams(_i2.SignInInitParameters? params) => + (super.noSuchMethod(Invocation.method(#initWithParams, [params]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future<_i2.GoogleSignInUserData?> signInSilently() => + (super.noSuchMethod(Invocation.method(#signInSilently, []), + returnValue: Future<_i2.GoogleSignInUserData?>.value()) + as _i4.Future<_i2.GoogleSignInUserData?>); + @override + _i4.Future<_i2.GoogleSignInUserData?> signIn() => + (super.noSuchMethod(Invocation.method(#signIn, []), + returnValue: Future<_i2.GoogleSignInUserData?>.value()) + as _i4.Future<_i2.GoogleSignInUserData?>); + @override + _i4.Future<_i2.GoogleSignInTokenData> getTokens( + {String? email, bool? shouldRecoverAuth}) => + (super.noSuchMethod( + Invocation.method(#getTokens, [], + {#email: email, #shouldRecoverAuth: shouldRecoverAuth}), + returnValue: Future<_i2.GoogleSignInTokenData>.value( + _FakeGoogleSignInTokenData_0())) + as _i4.Future<_i2.GoogleSignInTokenData>); + @override + _i4.Future signOut() => + (super.noSuchMethod(Invocation.method(#signOut, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future disconnect() => + (super.noSuchMethod(Invocation.method(#disconnect, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future isSignedIn() => + (super.noSuchMethod(Invocation.method(#isSignedIn, []), + returnValue: Future.value(false)) as _i4.Future); + @override + _i4.Future clearAuthCache({String? token}) => (super.noSuchMethod( + Invocation.method(#clearAuthCache, [], {#token: token}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future requestScopes(List? scopes) => + (super.noSuchMethod(Invocation.method(#requestScopes, [scopes]), + returnValue: Future.value(false)) as _i4.Future); +} diff --git a/packages/google_sign_in/google_sign_in_android/AUTHORS b/packages/google_sign_in/google_sign_in_android/AUTHORS new file mode 100644 index 000000000000..35d24a5ae0b5 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Twin Sun, LLC diff --git a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md new file mode 100644 index 000000000000..342166a8a6af --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md @@ -0,0 +1,39 @@ +## 6.1.1 + +* Corrects typos in plugin error logs and removes not actionable warnings. +* Updates minimum Flutter version to 2.10. +* Updates play-services-auth version to 20.3.0. + +## 6.1.0 + +* Adds override for `GoogleSignIn.initWithParams` to handle new `forceCodeForRefreshToken` parameter. + +## 6.0.1 + +* Updates gradle version to 7.2.1 on Android. + +## 6.0.0 + +* Deprecates `clientId` and adds support for `serverClientId` instead. + Historically `clientId` was interpreted as `serverClientId`, but only on Android. On + other platforms it was interpreted as the OAuth `clientId` of the app. For backwards-compatibility + `clientId` will still be used as a server client ID if `serverClientId` is not provided. +* **BREAKING CHANGES**: + * Adds `serverClientId` parameter to `IDelegate.init` (Java). + +## 5.2.8 + +* Suppresses `deprecation` warnings (for using Android V1 embedding). + +## 5.2.7 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 5.2.6 + +* Switches to an internal method channel, rather than the default. + +## 5.2.5 + +* Splits from `google_sign_in` as a federated implementation. diff --git a/packages/google_sign_in/google_sign_in_android/LICENSE b/packages/google_sign_in/google_sign_in_android/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_sign_in/google_sign_in_android/README.md b/packages/google_sign_in/google_sign_in_android/README.md new file mode 100644 index 000000000000..5c7c70ede917 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/README.md @@ -0,0 +1,11 @@ +# google\_sign\_in\_android + +The Android implementation of [`google_sign_in`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `google_sign_in` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/google_sign_in +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/google_sign_in/google_sign_in/android/build.gradle b/packages/google_sign_in/google_sign_in_android/android/build.gradle similarity index 79% rename from packages/google_sign_in/google_sign_in/android/build.gradle rename to packages/google_sign_in/google_sign_in_android/android/build.gradle index 8a2b69cb1789..f3a324fefbc9 100644 --- a/packages/google_sign_in/google_sign_in/android/build.gradle +++ b/packages/google_sign_in/google_sign_in_android/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:7.2.1' } } @@ -29,6 +29,8 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { + // TODO(stuartmorgan): Enable when gradle is updated. + // disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' } @@ -48,8 +50,8 @@ android { } dependencies { - implementation 'com.google.android.gms:play-services-auth:20.0.1' + implementation 'com.google.android.gms:play-services-auth:20.3.0' implementation 'com.google.guava:guava:28.1-android' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:3.9.0' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-inline:4.7.0' } diff --git a/packages/google_sign_in/google_sign_in_android/android/settings.gradle b/packages/google_sign_in/google_sign_in_android/android/settings.gradle new file mode 100644 index 000000000000..35ebd0e2428a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'google_sign_in_android' diff --git a/packages/google_sign_in/google_sign_in/android/src/main/AndroidManifest.xml b/packages/google_sign_in/google_sign_in_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/google_sign_in/google_sign_in/android/src/main/AndroidManifest.xml rename to packages/google_sign_in/google_sign_in_android/android/src/main/AndroidManifest.xml diff --git a/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java similarity index 100% rename from packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java rename to packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java diff --git a/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java similarity index 100% rename from packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java rename to packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java diff --git a/packages/google_sign_in/google_sign_in/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 similarity index 91% rename from packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java rename to packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index 1be023c678bb..d345d4976c63 100644 --- a/packages/google_sign_in/google_sign_in/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 @@ -8,6 +8,7 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.google.android.gms.auth.GoogleAuthUtil; @@ -44,7 +45,7 @@ /** Google sign-in plugin for Flutter. */ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware { - private static final String CHANNEL_NAME = "plugins.flutter.io/google_sign_in"; + private static final String CHANNEL_NAME = "plugins.flutter.io/google_sign_in_android"; private static final String METHOD_INIT = "init"; private static final String METHOD_SIGN_IN_SILENTLY = "signInSilently"; @@ -76,6 +77,7 @@ public void initInstance( } @VisibleForTesting + @SuppressWarnings("deprecation") public void setUpRegistrar(PluginRegistry.Registrar registrar) { delegate.setUpRegistrar(registrar); } @@ -137,7 +139,16 @@ public void onMethodCall(MethodCall call, Result result) { List requestedScopes = call.argument("scopes"); String hostedDomain = call.argument("hostedDomain"); String clientId = call.argument("clientId"); - delegate.init(result, signInOption, requestedScopes, hostedDomain, clientId); + String serverClientId = call.argument("serverClientId"); + boolean forceCodeForRefreshToken = call.argument("forceCodeForRefreshToken"); + delegate.init( + result, + signInOption, + requestedScopes, + hostedDomain, + clientId, + serverClientId, + forceCodeForRefreshToken); break; case METHOD_SIGN_IN_SILENTLY: @@ -193,7 +204,9 @@ public void init( String signInOption, List requestedScopes, String hostedDomain, - String clientId); + String clientId, + String serverClientId, + boolean forceCodeForRefreshToken); /** * Returns the account information for the user who is signed in to this app. If no user is @@ -267,6 +280,7 @@ public static class Delegate implements IDelegate, PluginRegistry.ActivityResult private final Context context; // Only set registrar for v1 embedder. + @SuppressWarnings("deprecation") private PluginRegistry.Registrar registrar; // Only set activity for v2 embedder. Always access activity from getActivity() method. private Activity activity; @@ -282,6 +296,7 @@ public Delegate(Context context, GoogleSignInWrapper googleSignInWrapper) { this.googleSignInWrapper = googleSignInWrapper; } + @SuppressWarnings("deprecation") public void setUpRegistrar(PluginRegistry.Registrar registrar) { this.registrar = registrar; registrar.addActivityResultListener(this); @@ -318,7 +333,9 @@ public void init( String signInOption, List requestedScopes, String hostedDomain, - String clientId) { + String clientId, + String serverClientId, + boolean forceCodeForRefreshToken) { try { GoogleSignInOptions.Builder optionsBuilder; @@ -335,20 +352,34 @@ public void init( throw new IllegalStateException("Unknown signInOption"); } - // Only requests a clientId if google-services.json was present and parsed - // by the google-services Gradle script. - // TODO(jackson): Perhaps we should provide a mechanism to override this - // behavior. - int clientIdIdentifier = - context - .getResources() - .getIdentifier("default_web_client_id", "string", context.getPackageName()); - if (!Strings.isNullOrEmpty(clientId)) { - optionsBuilder.requestIdToken(clientId); - optionsBuilder.requestServerAuthCode(clientId); - } else if (clientIdIdentifier != 0) { - optionsBuilder.requestIdToken(context.getString(clientIdIdentifier)); - optionsBuilder.requestServerAuthCode(context.getString(clientIdIdentifier)); + // The clientId parameter is not supported on Android. + // Android apps are identified by their package name and the SHA-1 of their signing key. + // https://developers.google.com/android/guides/client-auth + // https://developers.google.com/identity/sign-in/android/start#configure-a-google-api-project + if (!Strings.isNullOrEmpty(clientId) && Strings.isNullOrEmpty(serverClientId)) { + Log.w( + "google_sign_in", + "clientId is not supported on Android and is interpreted as serverClientId. " + + "Use serverClientId instead to suppress this warning."); + serverClientId = clientId; + } + + if (Strings.isNullOrEmpty(serverClientId)) { + // Only requests a clientId if google-services.json was present and parsed + // by the google-services Gradle script. + // TODO(jackson): Perhaps we should provide a mechanism to override this + // behavior. + int webClientIdIdentifier = + context + .getResources() + .getIdentifier("default_web_client_id", "string", context.getPackageName()); + if (webClientIdIdentifier != 0) { + serverClientId = context.getString(webClientIdIdentifier); + } + } + if (!Strings.isNullOrEmpty(serverClientId)) { + optionsBuilder.requestIdToken(serverClientId); + optionsBuilder.requestServerAuthCode(serverClientId, forceCodeForRefreshToken); } for (String scope : requestedScopes) { optionsBuilder.requestScopes(new Scope(scope)); @@ -358,7 +389,7 @@ public void init( } this.requestedScopes = requestedScopes; - signInClient = GoogleSignIn.getClient(context, optionsBuilder.build()); + signInClient = googleSignInWrapper.getClient(context, optionsBuilder.build()); result.success(null); } catch (Exception e) { result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); diff --git a/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java similarity index 83% rename from packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java rename to packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java index 5af0b50136ce..c035329f8e96 100644 --- a/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java @@ -8,6 +8,8 @@ import android.content.Context; import com.google.android.gms.auth.api.signin.GoogleSignIn; 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.Scope; /** @@ -21,6 +23,10 @@ */ public class GoogleSignInWrapper { + GoogleSignInClient getClient(Context context, GoogleSignInOptions options) { + return GoogleSignIn.getClient(context, options); + } + GoogleSignInAccount getLastSignedInAccount(Context context) { return GoogleSignIn.getLastSignedInAccount(context); } diff --git a/packages/google_sign_in/google_sign_in/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 similarity index 59% rename from packages/google_sign_in/google_sign_in/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java rename to packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java index 3b6ad960f548..9692417390a5 100644 --- a/packages/google_sign_in/google_sign_in/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 @@ -4,6 +4,7 @@ package io.flutter.plugins.googlesignin; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -12,7 +13,10 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.res.Resources; 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.Scope; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; @@ -21,6 +25,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -30,12 +35,14 @@ public class GoogleSignInTest { @Mock Context mockContext; + @Mock Resources mockResources; @Mock Activity mockActivity; @Mock PluginRegistry.Registrar mockRegistrar; @Mock BinaryMessenger mockMessenger; @Spy MethodChannel.Result result; @Mock GoogleSignInWrapper mockGoogleSignIn; @Mock GoogleSignInAccount account; + @Mock GoogleSignInClient mockClient; private GoogleSignInPlugin plugin; @Before @@ -44,6 +51,7 @@ public void setUp() { when(mockRegistrar.messenger()).thenReturn(mockMessenger); when(mockRegistrar.context()).thenReturn(mockContext); when(mockRegistrar.activity()).thenReturn(mockActivity); + when(mockContext.getResources()).thenReturn(mockResources); plugin = new GoogleSignInPlugin(); plugin.initInstance(mockRegistrar.messenger(), mockRegistrar.context(), mockGoogleSignIn); plugin.setUpRegistrar(mockRegistrar); @@ -195,4 +203,137 @@ public void signInThrowsWithoutActivity() { plugin.onMethodCall(new MethodCall("signIn", null), null); } + + @Test + public void init_LoadsServerClientIdFromResources() { + final String packageName = "fakePackageName"; + final String serverClientId = "fakeServerClientId"; + final int resourceId = 1; + MethodCall methodCall = buildInitMethodCall(null, null); + when(mockContext.getPackageName()).thenReturn(packageName); + when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) + .thenReturn(resourceId); + when(mockContext.getString(resourceId)).thenReturn(serverClientId); + initAndAssertServerClientId(methodCall, serverClientId); + } + + @Test + public void init_InterpretsClientIdAsServerClientId() { + final String clientId = "fakeClientId"; + MethodCall methodCall = buildInitMethodCall(clientId, null); + initAndAssertServerClientId(methodCall, clientId); + } + + @Test + public void init_ForwardsServerClientId() { + final String serverClientId = "fakeServerClientId"; + MethodCall methodCall = buildInitMethodCall(null, serverClientId); + initAndAssertServerClientId(methodCall, serverClientId); + } + + @Test + public void init_IgnoresClientIdIfServerClientIdIsProvided() { + final String clientId = "fakeClientId"; + final String serverClientId = "fakeServerClientId"; + MethodCall methodCall = buildInitMethodCall(clientId, serverClientId); + initAndAssertServerClientId(methodCall, serverClientId); + } + + @Test + public void init_PassesForceCodeForRefreshTokenFalseWithServerClientIdParameter() { + MethodCall methodCall = buildInitMethodCall("fakeClientId", "fakeServerClientId", false); + + initAndAssertForceCodeForRefreshToken(methodCall, false); + } + + @Test + public void init_PassesForceCodeForRefreshTokenTrueWithServerClientIdParameter() { + MethodCall methodCall = buildInitMethodCall("fakeClientId", "fakeServerClientId", true); + + initAndAssertForceCodeForRefreshToken(methodCall, true); + } + + @Test + public void init_PassesForceCodeForRefreshTokenFalseWithServerClientIdFromResources() { + final String packageName = "fakePackageName"; + final String serverClientId = "fakeServerClientId"; + final int resourceId = 1; + MethodCall methodCall = buildInitMethodCall(null, null, false); + when(mockContext.getPackageName()).thenReturn(packageName); + when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) + .thenReturn(resourceId); + when(mockContext.getString(resourceId)).thenReturn(serverClientId); + + initAndAssertForceCodeForRefreshToken(methodCall, false); + } + + @Test + public void init_PassesForceCodeForRefreshTokenTrueWithServerClientIdFromResources() { + final String packageName = "fakePackageName"; + final String serverClientId = "fakeServerClientId"; + final int resourceId = 1; + MethodCall methodCall = buildInitMethodCall(null, null, true); + when(mockContext.getPackageName()).thenReturn(packageName); + when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) + .thenReturn(resourceId); + when(mockContext.getString(resourceId)).thenReturn(serverClientId); + + initAndAssertForceCodeForRefreshToken(methodCall, true); + } + + public void initAndAssertServerClientId(MethodCall methodCall, String serverClientId) { + ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(GoogleSignInOptions.class); + when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture())) + .thenReturn(mockClient); + plugin.onMethodCall(methodCall, result); + verify(result).success(null); + Assert.assertEquals(serverClientId, optionsCaptor.getValue().getServerClientId()); + } + + public void initAndAssertForceCodeForRefreshToken( + MethodCall methodCall, boolean forceCodeForRefreshToken) { + ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(GoogleSignInOptions.class); + when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture())) + .thenReturn(mockClient); + plugin.onMethodCall(methodCall, result); + verify(result).success(null); + Assert.assertEquals( + forceCodeForRefreshToken, optionsCaptor.getValue().isForceCodeForRefreshToken()); + } + + private static MethodCall buildInitMethodCall(String clientId, String serverClientId) { + return buildInitMethodCall( + "SignInOption.standard", Collections.emptyList(), clientId, serverClientId, false); + } + + private static MethodCall buildInitMethodCall( + String clientId, String serverClientId, boolean forceCodeForRefreshToken) { + return buildInitMethodCall( + "SignInOption.standard", + Collections.emptyList(), + clientId, + serverClientId, + forceCodeForRefreshToken); + } + + private static MethodCall buildInitMethodCall( + String signInOption, + List scopes, + String clientId, + String serverClientId, + boolean forceCodeForRefreshToken) { + HashMap arguments = new HashMap<>(); + arguments.put("signInOption", signInOption); + arguments.put("scopes", scopes); + if (clientId != null) { + arguments.put("clientId", clientId); + } + if (serverClientId != null) { + arguments.put("serverClientId", serverClientId); + } + arguments.put("forceCodeForRefreshToken", forceCodeForRefreshToken); + return new MethodCall("init", arguments); + } } diff --git a/packages/google_sign_in/google_sign_in_android/example/README.md b/packages/google_sign_in/google_sign_in_android/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle b/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle new file mode 100644 index 000000000000..8ac99fe56f3a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle @@ -0,0 +1,67 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.googlesigninexample" + minSdkVersion 16 + targetSdkVersion 28 + multiDexEnabled true + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } + + testOptions { + unitTests.returnDefaultValues = true + } +} + +flutter { + source '../..' +} + +dependencies { + implementation 'com.google.android.gms:play-services-auth:16.0.1' + testImplementation'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' +} diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/google-services.json b/packages/google_sign_in/google_sign_in_android/example/android/app/google-services.json new file mode 100644 index 000000000000..efa524535553 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/google-services.json @@ -0,0 +1,246 @@ +{ + "project_info": { + "project_number": "479882132969", + "firebase_url": "https://my-flutter-proj.firebaseio.com", + "project_id": "my-flutter-proj", + "storage_bucket": "my-flutter-proj.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:479882132969:android:c73fd19ff7e2c0be", + "android_client_info": { + "package_name": "io.flutter.plugins.cameraexample" + } + }, + "oauth_client": [ + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 1, + "other_platform_oauth_client": [] + }, + "ads_service": { + "status": 2 + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:479882132969:android:632cdf3fc0a17139", + "android_client_info": { + "package_name": "io.flutter.plugins.firebasedynamiclinksexample" + } + }, + "oauth_client": [ + { + "client_id": "479882132969-32qusitiag53931ck80h121ajhlc5a7e.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.flutter.plugins.firebasedynamiclinksexample", + "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" + } + }, + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 2, + "other_platform_oauth_client": [ + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseMlVisionExample" + } + } + ] + }, + "ads_service": { + "status": 2 + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:479882132969:android:ae50362b4bc06086", + "android_client_info": { + "package_name": "io.flutter.plugins.firebasemlvisionexample" + } + }, + "oauth_client": [ + { + "client_id": "479882132969-9pp74fkgmtvt47t9rikc1p861v7n85tn.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.flutter.plugins.firebasemlvisionexample", + "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" + } + }, + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 2, + "other_platform_oauth_client": [ + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseMlVisionExample" + } + } + ] + }, + "ads_service": { + "status": 2 + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:479882132969:android:215a22700e1b466b", + "android_client_info": { + "package_name": "io.flutter.plugins.firebaseperformanceexample" + } + }, + "oauth_client": [ + { + "client_id": "479882132969-8h4kiv8m7ho4tvn6uuujsfcrf69unuf7.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.flutter.plugins.firebaseperformanceexample", + "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" + } + }, + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 2, + "other_platform_oauth_client": [ + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseMlVisionExample" + } + } + ] + }, + "ads_service": { + "status": 2 + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:479882132969:android:5e9f1f89e134dc86", + "android_client_info": { + "package_name": "io.flutter.plugins.googlesigninexample" + } + }, + "oauth_client": [ + { + "client_id": "479882132969-90ml692hkonp587sl0v0rurmnvkekgrg.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.flutter.plugins.googlesigninexample", + "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" + } + }, + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 2, + "other_platform_oauth_client": [ + { + "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseMlVisionExample" + } + } + ] + }, + "ads_service": { + "status": 2 + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in_android/example/android/app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..29e413457635 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -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. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java b/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java new file mode 100644 index 000000000000..edc01de491af --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlesigninexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java similarity index 100% rename from packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java rename to packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/debug/AndroidManifest.xml b/packages/google_sign_in/google_sign_in_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..4d764900a530 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/AndroidManifest.xml b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..22a34d7218f7 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/java/io/flutter/plugins/.gitignore b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/java/io/flutter/plugins/.gitignore new file mode 100644 index 000000000000..9eb4563d2ae1 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/java/io/flutter/plugins/.gitignore @@ -0,0 +1 @@ +GeneratedPluginRegistrant.java diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/GoogleSignInTestActivity.java b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/GoogleSignInTestActivity.java new file mode 100644 index 000000000000..09506a2632df --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/GoogleSignInTestActivity.java @@ -0,0 +1,20 @@ +// 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.googlesigninexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class GoogleSignInTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/values/strings.xml b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000000..c7e28ffcedd1 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + YOUR_WEB_CLIENT_ID + diff --git a/packages/google_sign_in/google_sign_in_android/example/android/build.gradle b/packages/google_sign_in/google_sign_in_android/example/android/build.gradle new file mode 100644 index 000000000000..c21bff8e0a2f --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.0.1' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} 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 new file mode 100644 index 000000000000..d12b9a8297e5 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true diff --git a/packages/google_sign_in/google_sign_in_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..297f2fec363f --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +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 diff --git a/packages/google_sign_in/google_sign_in_android/example/android/settings.gradle b/packages/google_sign_in/google_sign_in_android/example/android/settings.gradle new file mode 100644 index 000000000000..115da6cb4f4d --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withInputStream { stream -> plugins.load(stream) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart new file mode 100644 index 000000000000..f1388ce86d67 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart @@ -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. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can initialize the plugin', (WidgetTester tester) async { + final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; + expect(signIn, isNotNull); + }); + + testWidgets('Method channel handler is present', (WidgetTester tester) async { + // isSignedIn can be called without initialization, so use it to validate + // that the native method handler is present (e.g., that the channel name + // is correct). + final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; + await expectLater(signIn.isSignedIn(), completes); + }); +} diff --git a/packages/google_sign_in/google_sign_in_android/example/lib/main.dart b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart new file mode 100644 index 000000000000..5818b6040fcc --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart @@ -0,0 +1,183 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert' show json; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:http/http.dart' as http; + +void main() { + runApp( + const MaterialApp( + title: 'Google Sign In', + home: SignInDemo(), + ), + ); +} + +class SignInDemo extends StatefulWidget { + const SignInDemo({Key? key}) : super(key: key); + + @override + State createState() => SignInDemoState(); +} + +class SignInDemoState extends State { + GoogleSignInUserData? _currentUser; + String _contactText = ''; + // Future that completes when `init` has completed on the sign in instance. + Future? _initialization; + + @override + void initState() { + super.initState(); + _signIn(); + } + + Future _ensureInitialized() { + return _initialization ??= + GoogleSignInPlatform.instance.initWithParams(const SignInInitParameters( + scopes: [ + 'email', + 'https://www.googleapis.com/auth/contacts.readonly', + ], + )) + ..catchError((dynamic _) { + _initialization = null; + }); + } + + void _setUser(GoogleSignInUserData? user) { + setState(() { + _currentUser = user; + if (user != null) { + _handleGetContact(user); + } + }); + } + + Future _signIn() async { + await _ensureInitialized(); + final GoogleSignInUserData? newUser = + await GoogleSignInPlatform.instance.signInSilently(); + _setUser(newUser); + } + + Future> _getAuthHeaders() async { + final GoogleSignInUserData? user = _currentUser; + if (user == null) { + throw StateError('No user signed in'); + } + + final GoogleSignInTokenData response = + await GoogleSignInPlatform.instance.getTokens( + email: user.email, + shouldRecoverAuth: true, + ); + + return { + 'Authorization': 'Bearer ${response.accessToken}', + // TODO(kevmoo): Use the correct value once it's available. + // See https://github.com/flutter/flutter/issues/80905 + 'X-Goog-AuthUser': '0', + }; + } + + Future _handleGetContact(GoogleSignInUserData user) async { + setState(() { + _contactText = 'Loading contact info...'; + }); + final http.Response response = await http.get( + Uri.parse('https://people.googleapis.com/v1/people/me/connections' + '?requestMask.includeField=person.names'), + headers: await _getAuthHeaders(), + ); + if (response.statusCode != 200) { + setState(() { + _contactText = 'People API gave a ${response.statusCode} ' + 'response. Check logs for details.'; + }); + print('People API ${response.statusCode} response: ${response.body}'); + return; + } + final Map data = + json.decode(response.body) as Map; + final int contactCount = + (data['connections'] as List?)?.length ?? 0; + setState(() { + _contactText = '$contactCount contacts found'; + }); + } + + Future _handleSignIn() async { + try { + await _ensureInitialized(); + _setUser(await GoogleSignInPlatform.instance.signIn()); + } catch (error) { + final bool canceled = + error is PlatformException && error.code == 'sign_in_canceled'; + if (!canceled) { + print(error); + } + } + } + + Future _handleSignOut() async { + await _ensureInitialized(); + await GoogleSignInPlatform.instance.disconnect(); + } + + Widget _buildBody() { + final GoogleSignInUserData? user = _currentUser; + if (user != null) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ListTile( + title: Text(user.displayName ?? ''), + subtitle: Text(user.email), + ), + const Text('Signed in successfully.'), + Text(_contactText), + ElevatedButton( + onPressed: _handleSignOut, + child: const Text('SIGN OUT'), + ), + ElevatedButton( + child: const Text('REFRESH'), + onPressed: () => _handleGetContact(user), + ), + ], + ); + } else { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Text('You are not currently signed in.'), + ElevatedButton( + onPressed: _handleSignIn, + child: const Text('SIGN IN'), + ), + ], + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Google Sign In'), + ), + body: ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: _buildBody(), + )); + } +} 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 new file mode 100644 index 000000000000..5ac2240cbba1 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml @@ -0,0 +1,32 @@ +name: google_sign_in_example +description: Example of Google Sign-In plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + flutter: + sdk: flutter + google_sign_in_android: + # When depending on this package from a real application you should use: + # google_sign_in_android: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + google_sign_in_platform_interface: ^2.2.0 + http: ^0.13.0 + +dev_dependencies: + espresso: ^0.2.0 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/google_sign_in/google_sign_in_android/example/test_driver/integration_test.dart b/packages/google_sign_in/google_sign_in_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/test_driver/integration_test.dart @@ -0,0 +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. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart new file mode 100644 index 000000000000..731da3968b3f --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -0,0 +1,106 @@ +// 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' show visibleForTesting; +import 'package:flutter/services.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +import 'src/utils.dart'; + +/// Android implementation of [GoogleSignInPlatform]. +class GoogleSignInAndroid extends GoogleSignInPlatform { + /// 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 + MethodChannel channel = + const MethodChannel('plugins.flutter.io/google_sign_in_android'); + + /// Registers this class as the default instance of [GoogleSignInPlatform]. + static void registerWith() { + GoogleSignInPlatform.instance = GoogleSignInAndroid(); + } + + @override + Future init({ + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String? hostedDomain, + String? clientId, + }) { + return initWithParams(SignInInitParameters( + signInOption: signInOption, + scopes: scopes, + hostedDomain: hostedDomain, + clientId: clientId, + )); + } + + @override + Future initWithParams(SignInInitParameters params) { + return channel.invokeMethod('init', { + 'signInOption': params.signInOption.toString(), + 'scopes': params.scopes, + 'hostedDomain': params.hostedDomain, + 'clientId': params.clientId, + 'forceCodeForRefreshToken': params.forceCodeForRefreshToken, + }); + } + + @override + Future signInSilently() { + return channel + .invokeMapMethod('signInSilently') + .then(getUserDataFromMap); + } + + @override + Future signIn() { + return channel + .invokeMapMethod('signIn') + .then(getUserDataFromMap); + } + + @override + Future getTokens( + {required String email, bool? shouldRecoverAuth = true}) { + return channel + .invokeMapMethod('getTokens', { + 'email': email, + 'shouldRecoverAuth': shouldRecoverAuth, + }).then((Map? result) => getTokenDataFromMap(result!)); + } + + @override + Future signOut() { + return channel.invokeMapMethod('signOut'); + } + + @override + Future disconnect() { + return channel.invokeMapMethod('disconnect'); + } + + @override + Future isSignedIn() async { + return (await channel.invokeMethod('isSignedIn'))!; + } + + @override + Future clearAuthCache({String? token}) { + return channel.invokeMethod( + 'clearAuthCache', + {'token': token}, + ); + } + + @override + Future requestScopes(List scopes) async { + return (await channel.invokeMethod( + 'requestScopes', + >{'scopes': scopes}, + ))!; + } +} diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_android/lib/src/utils.dart new file mode 100644 index 000000000000..5cd7c20b829a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/lib/src/utils.dart @@ -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 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +/// Converts user data coming from native code into the proper platform interface type. +GoogleSignInUserData? getUserDataFromMap(Map? data) { + if (data == null) { + return null; + } + return GoogleSignInUserData( + email: data['email']! as String, + id: data['id']! as String, + displayName: data['displayName'] as String?, + photoUrl: data['photoUrl'] as String?, + idToken: data['idToken'] as String?, + serverAuthCode: data['serverAuthCode'] as String?); +} + +/// Converts token data coming from native code into the proper platform interface type. +GoogleSignInTokenData getTokenDataFromMap(Map data) { + return GoogleSignInTokenData( + idToken: data['idToken'] as String?, + accessToken: data['accessToken'] as String?, + serverAuthCode: data['serverAuthCode'] as String?, + ); +} diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml new file mode 100644 index 000000000000..086a15117c2e --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml @@ -0,0 +1,36 @@ +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.1 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: google_sign_in + platforms: + android: + dartPluginClass: GoogleSignInAndroid + package: io.flutter.plugins.googlesignin + pluginClass: GoogleSignInPlugin + +dependencies: + flutter: + sdk: flutter + google_sign_in_platform_interface: ^2.2.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +# The example deliberately includes limited-use secrets. +false_secrets: + - /example/android/app/google-services.json + - /example/lib/main.dart 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 new file mode 100644 index 000000000000..948ced3f65bc --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart @@ -0,0 +1,158 @@ +// 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'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_android/google_sign_in_android.dart'; +import 'package:google_sign_in_android/src/utils.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +const Map kUserData = { + 'email': 'john.doe@gmail.com', + 'id': '8162538176523816253123', + 'photoUrl': 'https://lh5.googleusercontent.com/photo.jpg', + 'displayName': 'John Doe', + 'idToken': '123', + 'serverAuthCode': '789', +}; + +const Map kTokenData = { + 'idToken': '123', + 'accessToken': '456', + 'serverAuthCode': '789', +}; + +const Map kDefaultResponses = { + 'init': null, + 'signInSilently': kUserData, + 'signIn': kUserData, + 'signOut': null, + 'disconnect': null, + 'isSignedIn': true, + 'getTokens': kTokenData, + 'requestScopes': true, +}; + +final GoogleSignInUserData? kUser = getUserDataFromMap(kUserData); +final GoogleSignInTokenData kToken = + getTokenDataFromMap(kTokenData as Map); + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final GoogleSignInAndroid googleSignIn = GoogleSignInAndroid(); + final MethodChannel channel = googleSignIn.channel; + + final List log = []; + late Map + responses; // Some tests mutate some kDefaultResponses + + 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); + }); + log.clear(); + }); + + test('registered instance', () { + GoogleSignInAndroid.registerWith(); + expect(GoogleSignInPlatform.instance, isA()); + }); + + test('signInSilently transforms platform data to GoogleSignInUserData', + () async { + final dynamic response = await googleSignIn.signInSilently(); + expect(response, kUser); + }); + test('signInSilently Exceptions -> throws', () async { + responses['signInSilently'] = Exception('Not a user'); + expect(googleSignIn.signInSilently(), + throwsA(isInstanceOf())); + }); + + test('signIn transforms platform data to GoogleSignInUserData', () async { + final dynamic response = await googleSignIn.signIn(); + expect(response, kUser); + }); + test('signIn Exceptions -> throws', () async { + responses['signIn'] = Exception('Not a user'); + expect(googleSignIn.signIn(), throwsA(isInstanceOf())); + }); + + test('getTokens transforms platform data to GoogleSignInTokenData', () async { + final dynamic response = await googleSignIn.getTokens( + email: 'example@example.com', shouldRecoverAuth: false); + expect(response, kToken); + expect( + log[0], + isMethodCall('getTokens', arguments: { + 'email': 'example@example.com', + 'shouldRecoverAuth': false, + })); + }); + + test('Other functions pass through arguments to the channel', () async { + final Map tests = { + () { + googleSignIn.init( + hostedDomain: 'example.com', + scopes: ['two', 'scopes'], + signInOption: SignInOption.games, + clientId: 'fakeClientId'); + }: isMethodCall('init', arguments: { + 'hostedDomain': 'example.com', + 'scopes': ['two', 'scopes'], + 'signInOption': 'SignInOption.games', + 'clientId': 'fakeClientId', + 'forceCodeForRefreshToken': false, + }), + () { + googleSignIn.initWithParams(const SignInInitParameters( + hostedDomain: 'example.com', + scopes: ['two', 'scopes'], + signInOption: SignInOption.games, + clientId: 'fakeClientId', + forceCodeForRefreshToken: true)); + }: isMethodCall('init', arguments: { + 'hostedDomain': 'example.com', + 'scopes': ['two', 'scopes'], + 'signInOption': 'SignInOption.games', + 'clientId': 'fakeClientId', + 'forceCodeForRefreshToken': true, + }), + () { + googleSignIn.getTokens( + email: 'example@example.com', shouldRecoverAuth: false); + }: isMethodCall('getTokens', arguments: { + 'email': 'example@example.com', + 'shouldRecoverAuth': false, + }), + () { + googleSignIn.clearAuthCache(token: 'abc'); + }: isMethodCall('clearAuthCache', arguments: { + 'token': 'abc', + }), + () { + googleSignIn.requestScopes(['newScope', 'anotherScope']); + }: isMethodCall('requestScopes', arguments: { + 'scopes': ['newScope', 'anotherScope'], + }), + googleSignIn.signOut: isMethodCall('signOut', arguments: null), + googleSignIn.disconnect: isMethodCall('disconnect', arguments: null), + googleSignIn.isSignedIn: isMethodCall('isSignedIn', arguments: null), + }; + + for (final Function f in tests.keys) { + f(); + } + + expect(log, tests.values); + }); +} diff --git a/packages/google_sign_in/google_sign_in_ios/AUTHORS b/packages/google_sign_in/google_sign_in_ios/AUTHORS new file mode 100644 index 000000000000..35d24a5ae0b5 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Twin Sun, LLC diff --git a/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md new file mode 100644 index 000000000000..ecb3b6bee039 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md @@ -0,0 +1,33 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 5.5.0 + +* Adds override for `GoogleSignInPlatform.initWithParams`. + +## 5.4.0 + +* Adds support for `serverClientId` configuration option. +* Makes `Google-Services.info` file optional. + +## 5.3.1 + +* Suppresses warnings for pre-iOS-13 codepaths. + +## 5.3.0 + +* Supports arm64 iOS simulators by increasing GoogleSignIn dependency to version 6.2. + +## 5.2.7 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 5.2.6 + +* Switches to an internal method channel, rather than the default. + +## 5.2.5 + +* Splits from `google_sign_in` as a federated implementation. diff --git a/packages/google_sign_in/google_sign_in_ios/LICENSE b/packages/google_sign_in/google_sign_in_ios/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_sign_in/google_sign_in_ios/README.md b/packages/google_sign_in/google_sign_in_ios/README.md new file mode 100644 index 000000000000..25e08fdb4040 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/README.md @@ -0,0 +1,11 @@ +# google\_sign\_in\_ios + +The iOS implementation of [`google_sign_in`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `google_sign_in` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/google_sign_in +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/google_sign_in/google_sign_in_ios/example/README.md b/packages/google_sign_in/google_sign_in_ios/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart new file mode 100644 index 000000000000..f1388ce86d67 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart @@ -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. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can initialize the plugin', (WidgetTester tester) async { + final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; + expect(signIn, isNotNull); + }); + + testWidgets('Method channel handler is present', (WidgetTester tester) async { + // isSignedIn can be called without initialization, so use it to validate + // that the native method handler is present (e.g., that the channel name + // is correct). + final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; + await expectLater(signIn.isSignedIn(), completes); + }); +} diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 9.0 + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/Debug.xcconfig b/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..9803018ca79d --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Generated.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/Release.xcconfig b/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..a4a8c604e13d --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Generated.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/google_sign_in/google_sign_in/example/ios/RunnerUITests/Info.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/GoogleSignInPluginTest/Info.plist similarity index 100% rename from packages/google_sign_in/google_sign_in/example/ios/RunnerUITests/Info.plist rename to packages/google_sign_in/google_sign_in_ios/example/ios/GoogleSignInPluginTest/Info.plist diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Podfile b/packages/google_sign_in/google_sign_in_ios/example/ios/Podfile new file mode 100644 index 000000000000..b95dfa75ea04 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Podfile @@ -0,0 +1,47 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +# Suppress warnings from transitive dependencies that cause analysis to fail. +pod 'AppAuth', :inhibit_warnings => true +pod 'GTMAppAuth', :inhibit_warnings => true + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + + pod 'OCMock','3.5' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..a7f2019ac311 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,736 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 5C6F5A6E1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6F5A6D1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m */; }; + 7A303C2E1E89D76400B1F19E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7A303C2D1E89D76400B1F19E /* GoogleService-Info.plist */; }; + 7ACDFB0E1E8944C400BE2D00 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7ACDFB0D1E8944C400BE2D00 /* AppFrameworkInfo.plist */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + C2FB9CBA01DB0A2DE5F31E12 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0263E28FA425D1CE928BDE15 /* libPods-Runner.a */; }; + C56D3B06A42F3B35C1F47A43 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 18AD6475292B9C45B529DDC9 /* libPods-RunnerTests.a */; }; + F76AC1A52666D0540040C8BC /* GoogleSignInTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC1A42666D0540040C8BC /* GoogleSignInTests.m */; }; + F76AC1B32666D0610040C8BC /* GoogleSignInUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC1B22666D0610040C8BC /* GoogleSignInUITests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F76AC1A72666D0540040C8BC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + F76AC1B52666D0610040C8BC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0263E28FA425D1CE928BDE15 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 18AD6475292B9C45B529DDC9 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 37E582FF620A90D0EB2C0851 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 45D93D4513839BFEA2AA74FE /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 5A76713E622F06379AEDEBFA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 5C6F5A6C1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 5C6F5A6D1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 7A303C2D1E89D76400B1F19E /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 7ACDFB0D1E8944C400BE2D00 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F582639B44581540871D9BB0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + F76AC1A22666D0540040C8BC /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F76AC1A42666D0540040C8BC /* GoogleSignInTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleSignInTests.m; sourceTree = ""; }; + F76AC1A62666D0540040C8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F76AC1B02666D0610040C8BC /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F76AC1B22666D0610040C8BC /* GoogleSignInUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleSignInUITests.m; sourceTree = ""; }; + F76AC1B42666D0610040C8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C2FB9CBA01DB0A2DE5F31E12 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC19F2666D0540040C8BC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C56D3B06A42F3B35C1F47A43 /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC1AD2666D0610040C8BC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { + isa = PBXGroup; + children = ( + 5A76713E622F06379AEDEBFA /* Pods-Runner.debug.xcconfig */, + F582639B44581540871D9BB0 /* Pods-Runner.release.xcconfig */, + 37E582FF620A90D0EB2C0851 /* Pods-RunnerTests.debug.xcconfig */, + 45D93D4513839BFEA2AA74FE /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 7ACDFB0D1E8944C400BE2D00 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + F76AC1A32666D0540040C8BC /* RunnerTests */, + F76AC1B12666D0610040C8BC /* RunnerUITests */, + 97C146EF1CF9000F007C117D /* Products */, + 840012C8B5EDBCF56B0E4AC1 /* Pods */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + F76AC1A22666D0540040C8BC /* RunnerTests.xctest */, + F76AC1B02666D0610040C8BC /* RunnerUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 5C6F5A6C1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.h */, + 5C6F5A6D1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m */, + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 7A303C2D1E89D76400B1F19E /* GoogleService-Info.plist */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0263E28FA425D1CE928BDE15 /* libPods-Runner.a */, + 18AD6475292B9C45B529DDC9 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + F76AC1A32666D0540040C8BC /* RunnerTests */ = { + isa = PBXGroup; + children = ( + F76AC1A42666D0540040C8BC /* GoogleSignInTests.m */, + F76AC1A62666D0540040C8BC /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + F76AC1B12666D0610040C8BC /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + F76AC1B22666D0610040C8BC /* GoogleSignInUITests.m */, + F76AC1B42666D0610040C8BC /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + F76AC1A12666D0540040C8BC /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F76AC1AB2666D0540040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 27975964E48117AA65B1D6C7 /* [CP] Check Pods Manifest.lock */, + F76AC19E2666D0540040C8BC /* Sources */, + F76AC19F2666D0540040C8BC /* Frameworks */, + F76AC1A02666D0540040C8BC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F76AC1A82666D0540040C8BC /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = F76AC1A22666D0540040C8BC /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F76AC1AF2666D0610040C8BC /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F76AC1B72666D0610040C8BC /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + F76AC1AC2666D0610040C8BC /* Sources */, + F76AC1AD2666D0610040C8BC /* Frameworks */, + F76AC1AE2666D0610040C8BC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F76AC1B62666D0610040C8BC /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = F76AC1B02666D0610040C8BC /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + F76AC1A12666D0540040C8BC = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + F76AC1AF2666D0610040C8BC = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + F76AC1A12666D0540040C8BC /* RunnerTests */, + F76AC1AF2666D0610040C8BC /* RunnerUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7A303C2E1E89D76400B1F19E /* GoogleService-Info.plist in Resources */, + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 7ACDFB0E1E8944C400BE2D00 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC1A02666D0540040C8BC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC1AE2666D0610040C8BC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 27975964E48117AA65B1D6C7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n"; + }; + 532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 5C6F5A6E1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC19E2666D0540040C8BC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F76AC1A52666D0540040C8BC /* GoogleSignInTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC1AC2666D0610040C8BC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F76AC1B32666D0610040C8BC /* GoogleSignInUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F76AC1A82666D0540040C8BC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F76AC1A72666D0540040C8BC /* PBXContainerItemProxy */; + }; + F76AC1B62666D0610040C8BC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F76AC1B52666D0610040C8BC /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.googleSignInExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.googleSignInExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + F76AC1A92666D0540040C8BC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 37E582FF620A90D0EB2C0851 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + 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"; + }; + name = Debug; + }; + F76AC1AA2666D0540040C8BC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 45D93D4513839BFEA2AA74FE /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + 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"; + }; + name = Release; + }; + F76AC1B82666D0610040C8BC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + F76AC1B92666D0610040C8BC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F76AC1AB2666D0540040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F76AC1A92666D0540040C8BC /* Debug */, + F76AC1AA2666D0540040C8BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F76AC1B72666D0610040C8BC /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F76AC1B82666D0610040C8BC /* Debug */, + F76AC1B92666D0610040C8BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..f4569c48ce10 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.h b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.h @@ -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 +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.m b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.m @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d22f10b2ab63 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..ebf48f603974 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/GoogleService-Info.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/GoogleService-Info.plist new file mode 100644 index 000000000000..6042aab908af --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,44 @@ + + + + + AD_UNIT_ID_FOR_BANNER_TEST + ca-app-pub-3940256099942544/2934735716 + AD_UNIT_ID_FOR_INTERSTITIAL_TEST + ca-app-pub-3940256099942544/4411468910 + CLIENT_ID + 479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u + ANDROID_CLIENT_ID + 479882132969-jie8r1me6dsra60pal6ejaj8dgme3tg0.apps.googleusercontent.com + API_KEY + AIzaSyBECOwLTAN6PU4Aet1b2QLGIb3kRK8Xjew + GCM_SENDER_ID + 479882132969 + PLIST_VERSION + 1 + BUNDLE_ID + io.flutter.plugins.googleSignInExample + PROJECT_ID + my-flutter-proj + STORAGE_BUCKET + my-flutter-proj.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:479882132969:ios:2643f950e0a0da08 + DATABASE_URL + https://my-flutter-proj.firebaseio.com + SERVER_CLIENT_ID + YOUR_SERVER_CLIENT_ID + + \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..187584d1cfd9 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist @@ -0,0 +1,64 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Google Sign-In Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + GoogleSignInExample + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.googleusercontent.apps.479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + + diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/main.m b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/main.m @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/GoogleSignInTests.m b/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/GoogleSignInTests.m new file mode 100644 index 000000000000..5738b7f1c1fc --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/GoogleSignInTests.m @@ -0,0 +1,745 @@ +// 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 Flutter; + +@import XCTest; +@import google_sign_in_ios; +@import google_sign_in_ios.Test; +@import GoogleSignIn; + +// OCMock library doesn't generate a valid modulemap. +#import + +@interface FLTGoogleSignInPluginTest : XCTestCase + +@property(strong, nonatomic) NSObject *mockBinaryMessenger; +@property(strong, nonatomic) NSObject *mockPluginRegistrar; +@property(strong, nonatomic) FLTGoogleSignInPlugin *plugin; +@property(strong, nonatomic) id mockSignIn; + +@end + +@implementation FLTGoogleSignInPluginTest + +- (void)setUp { + [super setUp]; + self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + self.mockPluginRegistrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + + id mockSignIn = OCMClassMock([GIDSignIn class]); + self.mockSignIn = mockSignIn; + + OCMStub(self.mockPluginRegistrar.messenger).andReturn(self.mockBinaryMessenger); + self.plugin = [[FLTGoogleSignInPlugin alloc] initWithSignIn:mockSignIn]; + [FLTGoogleSignInPlugin registerWithRegistrar:self.mockPluginRegistrar]; +} + +- (void)testUnimplementedMethod { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"bogus" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id result) { + XCTAssertEqualObjects(result, FlutterMethodNotImplemented); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testSignOut { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signOut" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerify([self.mockSignIn signOut]); +} + +- (void)testDisconnect { + [[self.mockSignIn stub] disconnectWithCallback:[OCMArg invokeBlockWithArgs:[NSNull null], nil]]; + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"disconnect" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result, @{}); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testDisconnectIgnoresError { + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeHasNoAuthInKeychain + userInfo:nil]; + [[self.mockSignIn stub] disconnectWithCallback:[OCMArg invokeBlockWithArgs:error, nil]]; + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"disconnect" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result, @{}); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +#pragma mark - Init + +- (void)testInitNoClientIdError { + // Init plugin without GoogleService-Info.plist. + self.plugin = [[FLTGoogleSignInPlugin alloc] initWithSignIn:self.mockSignIn + withGoogleServiceProperties:nil]; + + // init call does not provide a clientId. + FlutterMethodCall *initMethodCall = [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{}]; + + XCTestExpectation *initExpectation = + [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:initMethodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"missing-config"); + [initExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testInitGoogleServiceInfoPlist { + FlutterMethodCall *initMethodCall = + [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{@"hostedDomain" : @"example.com"}]; + + XCTestExpectation *initExpectation = + [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:initMethodCall + result:^(id result) { + XCTAssertNil(result); + [initExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + // Initialization values used in the next sign in request. + FlutterMethodCall *signInMethodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + [self.plugin handleMethodCall:signInMethodCall + result:^(id r){ + }]; + OCMVerify([self.mockSignIn + signInWithConfiguration:[OCMArg checkWithBlock:^BOOL(GIDConfiguration *configuration) { + // Set in example app GoogleService-Info.plist. + return + [configuration.hostedDomain isEqualToString:@"example.com"] && + [configuration.clientID + isEqualToString: + @"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"] && + [configuration.serverClientID isEqualToString:@"YOUR_SERVER_CLIENT_ID"]; + }] + presentingViewController:[OCMArg isKindOfClass:[FlutterViewController class]] + hint:nil + additionalScopes:OCMOCK_ANY + callback:OCMOCK_ANY]); +} + +- (void)testInitDynamicClientIdNullDomain { + // Init plugin without GoogleService-Info.plist. + self.plugin = [[FLTGoogleSignInPlugin alloc] initWithSignIn:self.mockSignIn + withGoogleServiceProperties:nil]; + + FlutterMethodCall *initMethodCall = [FlutterMethodCall + methodCallWithMethodName:@"init" + arguments:@{@"hostedDomain" : [NSNull null], @"clientId" : @"mockClientId"}]; + + XCTestExpectation *initExpectation = + [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:initMethodCall + result:^(id result) { + XCTAssertNil(result); + [initExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + // Initialization values used in the next sign in request. + FlutterMethodCall *signInMethodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + [self.plugin handleMethodCall:signInMethodCall + result:^(id r){ + }]; + OCMVerify([self.mockSignIn + signInWithConfiguration:[OCMArg checkWithBlock:^BOOL(GIDConfiguration *configuration) { + return configuration.hostedDomain == nil && + [configuration.clientID isEqualToString:@"mockClientId"]; + }] + presentingViewController:[OCMArg isKindOfClass:[FlutterViewController class]] + hint:nil + additionalScopes:OCMOCK_ANY + callback:OCMOCK_ANY]); +} + +- (void)testInitDynamicServerClientIdNullDomain { + FlutterMethodCall *initMethodCall = + [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{ + @"hostedDomain" : [NSNull null], + @"serverClientId" : @"mockServerClientId" + }]; + + XCTestExpectation *initExpectation = + [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:initMethodCall + result:^(id result) { + XCTAssertNil(result); + [initExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + // Initialization values used in the next sign in request. + FlutterMethodCall *signInMethodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + [self.plugin handleMethodCall:signInMethodCall + result:^(id r){ + }]; + OCMVerify([self.mockSignIn + signInWithConfiguration:[OCMArg checkWithBlock:^BOOL(GIDConfiguration *configuration) { + return configuration.hostedDomain == nil && + [configuration.serverClientID isEqualToString:@"mockServerClientId"]; + }] + presentingViewController:[OCMArg isKindOfClass:[FlutterViewController class]] + hint:nil + additionalScopes:OCMOCK_ANY + callback:OCMOCK_ANY]); +} + +#pragma mark - Is signed in + +- (void)testIsNotSignedIn { + OCMStub([self.mockSignIn hasPreviousSignIn]).andReturn(NO); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"isSignedIn" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result) { + XCTAssertFalse(result.boolValue); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testIsSignedIn { + OCMStub([self.mockSignIn hasPreviousSignIn]).andReturn(YES); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"isSignedIn" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result) { + XCTAssertTrue(result.boolValue); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +#pragma mark - Sign in silently + +- (void)testSignInSilently { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([mockUser userID]).andReturn(@"mockID"); + + [[self.mockSignIn stub] + restorePreviousSignInWithCallback:[OCMArg invokeBlockWithArgs:mockUser, [NSNull null], nil]]; + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signInSilently" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result[@"displayName"], [NSNull null]); + XCTAssertEqualObjects(result[@"email"], [NSNull null]); + XCTAssertEqualObjects(result[@"id"], @"mockID"); + XCTAssertEqualObjects(result[@"photoUrl"], [NSNull null]); + XCTAssertEqualObjects(result[@"serverAuthCode"], [NSNull null]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testSignInSilentlyWithError { + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeHasNoAuthInKeychain + userInfo:nil]; + + [[self.mockSignIn stub] + restorePreviousSignInWithCallback:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signInSilently" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_required"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +#pragma mark - Sign in + +- (void)testSignIn { + id mockUser = OCMClassMock([GIDGoogleUser class]); + id mockUserProfile = OCMClassMock([GIDProfileData class]); + OCMStub([mockUserProfile name]).andReturn(@"mockDisplay"); + OCMStub([mockUserProfile email]).andReturn(@"mock@example.com"); + OCMStub([mockUserProfile hasImage]).andReturn(YES); + OCMStub([mockUserProfile imageURLWithDimension:1337]) + .andReturn([NSURL URLWithString:@"https://example.com/profile.png"]); + + OCMStub([mockUser profile]).andReturn(mockUserProfile); + OCMStub([mockUser userID]).andReturn(@"mockID"); + OCMStub([mockUser serverAuthCode]).andReturn(@"mockAuthCode"); + + [[self.mockSignIn expect] + signInWithConfiguration:[OCMArg checkWithBlock:^BOOL(GIDConfiguration *configuration) { + return [configuration.clientID + isEqualToString: + @"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"]; + }] + presentingViewController:[OCMArg isKindOfClass:[FlutterViewController class]] + hint:nil + additionalScopes:@[] + callback:[OCMArg invokeBlockWithArgs:mockUser, [NSNull null], nil]]; + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin + handleMethodCall:methodCall + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result[@"displayName"], @"mockDisplay"); + XCTAssertEqualObjects(result[@"email"], @"mock@example.com"); + XCTAssertEqualObjects(result[@"id"], @"mockID"); + XCTAssertEqualObjects(result[@"photoUrl"], @"https://example.com/profile.png"); + XCTAssertEqualObjects(result[@"serverAuthCode"], @"mockAuthCode"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + OCMVerifyAll(self.mockSignIn); +} + +- (void)testSignInWithInitializedScopes { + FlutterMethodCall *initMethodCall = + [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{@"scopes" : @[ @"initial1", @"initial2" ]}]; + + XCTestExpectation *initExpectation = + [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:initMethodCall + result:^(id result) { + XCTAssertNil(result); + [initExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([mockUser userID]).andReturn(@"mockID"); + + [[self.mockSignIn expect] + signInWithConfiguration:OCMOCK_ANY + presentingViewController:OCMOCK_ANY + hint:nil + additionalScopes:[OCMArg checkWithBlock:^BOOL(NSArray *scopes) { + return [[NSSet setWithArray:scopes] + isEqualToSet:[NSSet setWithObjects:@"initial1", @"initial2", nil]]; + }] + callback:[OCMArg invokeBlockWithArgs:mockUser, [NSNull null], nil]]; + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result[@"id"], @"mockID"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + OCMVerifyAll(self.mockSignIn); +} + +- (void)testSignInAlreadyGranted { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([mockUser userID]).andReturn(@"mockID"); + + [[self.mockSignIn stub] + signInWithConfiguration:OCMOCK_ANY + presentingViewController:OCMOCK_ANY + hint:nil + additionalScopes:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:mockUser, [NSNull null], nil]]; + + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeScopesAlreadyGranted + userInfo:nil]; + [[self.mockSignIn stub] addScopes:OCMOCK_ANY + presentingViewController:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result[@"id"], @"mockID"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testSignInError { + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeCanceled + userInfo:nil]; + [[self.mockSignIn stub] + signInWithConfiguration:OCMOCK_ANY + presentingViewController:OCMOCK_ANY + hint:nil + additionalScopes:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_canceled"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testSignInException { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + OCMExpect([self.mockSignIn signInWithConfiguration:OCMOCK_ANY + presentingViewController:OCMOCK_ANY + hint:OCMOCK_ANY + additionalScopes:OCMOCK_ANY + callback:OCMOCK_ANY]) + .andThrow([NSException exceptionWithName:@"MockName" reason:@"MockReason" userInfo:nil]); + + __block FlutterError *error; + XCTAssertThrows([self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + error = result; + }]); + + XCTAssertEqualObjects(error.code, @"google_sign_in"); + XCTAssertEqualObjects(error.message, @"MockReason"); + XCTAssertEqualObjects(error.details, @"MockName"); +} + +#pragma mark - Get tokens + +- (void)testGetTokens { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + OCMStub([mockAuthentication idToken]).andReturn(@"mockIdToken"); + OCMStub([mockAuthentication accessToken]).andReturn(@"mockAccessToken"); + [[mockAuthentication stub] + doWithFreshTokens:[OCMArg invokeBlockWithArgs:mockAuthentication, [NSNull null], nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result[@"idToken"], @"mockIdToken"); + XCTAssertEqualObjects(result[@"accessToken"], @"mockAccessToken"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testGetTokensNoAuthKeychainError { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeHasNoAuthInKeychain + userInfo:nil]; + [[mockAuthentication stub] + doWithFreshTokens:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_required"); + XCTAssertEqualObjects(result.message, kGIDSignInErrorDomain); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testGetTokensCancelledError { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeCanceled + userInfo:nil]; + [[mockAuthentication stub] + doWithFreshTokens:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_canceled"); + XCTAssertEqualObjects(result.message, kGIDSignInErrorDomain); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testGetTokensURLError { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:nil]; + [[mockAuthentication stub] + doWithFreshTokens:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"network_error"); + XCTAssertEqualObjects(result.message, NSURLErrorDomain); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testGetTokensUnknownError { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + NSError *error = [NSError errorWithDomain:@"BogusDomain" code:42 userInfo:nil]; + [[mockAuthentication stub] + doWithFreshTokens:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_failed"); + XCTAssertEqualObjects(result.message, @"BogusDomain"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +#pragma mark - Request scopes + +- (void)testRequestScopesResultErrorIfNotSignedIn { + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeNoCurrentUser + userInfo:nil]; + [[self.mockSignIn stub] addScopes:@[ @"mockScope1" ] + presentingViewController:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : @[ @"mockScope1" ]}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_required"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testRequestScopesIfNoMissingScope { + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeScopesAlreadyGranted + userInfo:nil]; + [[self.mockSignIn stub] addScopes:@[ @"mockScope1" ] + presentingViewController:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : @[ @"mockScope1" ]}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result) { + XCTAssertTrue(result.boolValue); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testRequestScopesWithUnknownError { + NSError *error = [NSError errorWithDomain:@"BogusDomain" code:42 userInfo:nil]; + [[self.mockSignIn stub] addScopes:@[ @"mockScope1" ] + presentingViewController:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : @[ @"mockScope1" ]}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result) { + XCTAssertFalse(result.boolValue); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testRequestScopesException { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:nil]; + OCMExpect([self.mockSignIn addScopes:@[] presentingViewController:OCMOCK_ANY callback:OCMOCK_ANY]) + .andThrow([NSException exceptionWithName:@"MockName" reason:@"MockReason" userInfo:nil]); + + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"request_scopes"); + XCTAssertEqualObjects(result.message, @"MockReason"); + XCTAssertEqualObjects(result.details, @"MockName"); + }]; +} + +- (void)testRequestScopesReturnsFalseIfOnlySubsetGranted { + GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + NSArray *requestedScopes = @[ @"mockScope1", @"mockScope2" ]; + + // Only grant one of the two requested scopes. + OCMStub(mockUser.grantedScopes).andReturn(@[ @"mockScope1" ]); + + [[self.mockSignIn stub] addScopes:requestedScopes + presentingViewController:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:mockUser, [NSNull null], nil]]; + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : requestedScopes}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns false"]; + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result) { + XCTAssertFalse(result.boolValue); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testRequestsInitializedScopes { + FlutterMethodCall *initMethodCall = + [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{@"scopes" : @[ @"initial1", @"initial2" ]}]; + + XCTestExpectation *initExpectation = + [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:initMethodCall + result:^(id result) { + XCTAssertNil(result); + [initExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + // Include one of the initially requested scopes. + NSArray *addedScopes = @[ @"initial1", @"addScope1", @"addScope2" ]; + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : addedScopes}]; + + [self.plugin handleMethodCall:methodCall + result:^(id result){ + }]; + + // All four scopes are requested. + [[self.mockSignIn verify] + addScopes:[OCMArg checkWithBlock:^BOOL(NSArray *scopes) { + return [[NSSet setWithArray:scopes] + isEqualToSet:[NSSet setWithObjects:@"initial1", @"initial2", + @"addScope1", @"addScope2", nil]]; + }] + presentingViewController:OCMOCK_ANY + callback:OCMOCK_ANY]; +} + +- (void)testRequestScopesReturnsTrueIfGranted { + GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + NSArray *requestedScopes = @[ @"mockScope1", @"mockScope2" ]; + + // Grant both of the requested scopes. + OCMStub(mockUser.grantedScopes).andReturn(requestedScopes); + + [[self.mockSignIn stub] addScopes:requestedScopes + presentingViewController:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:mockUser, [NSNull null], nil]]; + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : requestedScopes}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result) { + XCTAssertTrue(result.boolValue); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +@end diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/Info.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/image_picker/image_picker/example/ios/RunnerTests/Info.plist rename to packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/Info.plist diff --git a/packages/google_sign_in/google_sign_in/example/ios/RunnerUITests/GoogleSignInUITests.m b/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerUITests/GoogleSignInUITests.m similarity index 100% rename from packages/google_sign_in/google_sign_in/example/ios/RunnerUITests/GoogleSignInUITests.m rename to packages/google_sign_in/google_sign_in_ios/example/ios/RunnerUITests/GoogleSignInUITests.m diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITests/Info.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerUITests/Info.plist similarity index 100% rename from packages/image_picker/image_picker/example/ios/RunnerUITests/Info.plist rename to packages/google_sign_in/google_sign_in_ios/example/ios/RunnerUITests/Info.plist diff --git a/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart b/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart new file mode 100644 index 000000000000..e23935ded1da --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart @@ -0,0 +1,184 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert' show json; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:http/http.dart' as http; + +void main() { + runApp( + const MaterialApp( + title: 'Google Sign In', + home: SignInDemo(), + ), + ); +} + +class SignInDemo extends StatefulWidget { + const SignInDemo({Key? key}) : super(key: key); + + @override + State createState() => SignInDemoState(); +} + +class SignInDemoState extends State { + GoogleSignInUserData? _currentUser; + String _contactText = ''; + // Future that completes when `initWithParams` has completed on the sign in + // instance. + Future? _initialization; + + @override + void initState() { + super.initState(); + _signIn(); + } + + Future _ensureInitialized() { + return _initialization ??= + GoogleSignInPlatform.instance.initWithParams(const SignInInitParameters( + scopes: [ + 'email', + 'https://www.googleapis.com/auth/contacts.readonly', + ], + )) + ..catchError((dynamic _) { + _initialization = null; + }); + } + + void _setUser(GoogleSignInUserData? user) { + setState(() { + _currentUser = user; + if (user != null) { + _handleGetContact(user); + } + }); + } + + Future _signIn() async { + await _ensureInitialized(); + final GoogleSignInUserData? newUser = + await GoogleSignInPlatform.instance.signInSilently(); + _setUser(newUser); + } + + Future> _getAuthHeaders() async { + final GoogleSignInUserData? user = _currentUser; + if (user == null) { + throw StateError('No user signed in'); + } + + final GoogleSignInTokenData response = + await GoogleSignInPlatform.instance.getTokens( + email: user.email, + shouldRecoverAuth: true, + ); + + return { + 'Authorization': 'Bearer ${response.accessToken}', + // TODO(kevmoo): Use the correct value once it's available. + // See https://github.com/flutter/flutter/issues/80905 + 'X-Goog-AuthUser': '0', + }; + } + + Future _handleGetContact(GoogleSignInUserData user) async { + setState(() { + _contactText = 'Loading contact info...'; + }); + final http.Response response = await http.get( + Uri.parse('https://people.googleapis.com/v1/people/me/connections' + '?requestMask.includeField=person.names'), + headers: await _getAuthHeaders(), + ); + if (response.statusCode != 200) { + setState(() { + _contactText = 'People API gave a ${response.statusCode} ' + 'response. Check logs for details.'; + }); + print('People API ${response.statusCode} response: ${response.body}'); + return; + } + final Map data = + json.decode(response.body) as Map; + final int contactCount = + (data['connections'] as List?)?.length ?? 0; + setState(() { + _contactText = '$contactCount contacts found'; + }); + } + + Future _handleSignIn() async { + try { + await _ensureInitialized(); + _setUser(await GoogleSignInPlatform.instance.signIn()); + } catch (error) { + final bool canceled = + error is PlatformException && error.code == 'sign_in_canceled'; + if (!canceled) { + print(error); + } + } + } + + Future _handleSignOut() async { + await _ensureInitialized(); + await GoogleSignInPlatform.instance.disconnect(); + } + + Widget _buildBody() { + final GoogleSignInUserData? user = _currentUser; + if (user != null) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ListTile( + title: Text(user.displayName ?? ''), + subtitle: Text(user.email), + ), + const Text('Signed in successfully.'), + Text(_contactText), + ElevatedButton( + onPressed: _handleSignOut, + child: const Text('SIGN OUT'), + ), + ElevatedButton( + child: const Text('REFRESH'), + onPressed: () => _handleGetContact(user), + ), + ], + ); + } else { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Text('You are not currently signed in.'), + ElevatedButton( + onPressed: _handleSignIn, + child: const Text('SIGN IN'), + ), + ], + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Google Sign In'), + ), + body: ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: _buildBody(), + )); + } +} 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 new file mode 100644 index 000000000000..aedc4b01aade --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml @@ -0,0 +1,31 @@ +name: google_sign_in_example +description: Example of Google Sign-In plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + flutter: + sdk: flutter + google_sign_in_ios: + # When depending on this package from a real application you should use: + # google_sign_in_ios: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + google_sign_in_platform_interface: ^2.2.0 + http: ^0.13.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/google_sign_in/google_sign_in_ios/example/test_driver/integration_test.dart b/packages/google_sign_in/google_sign_in_ios/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/test_driver/integration_test.dart @@ -0,0 +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. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/image_picker/image_picker/ios/Assets/.gitkeep b/packages/google_sign_in/google_sign_in_ios/ios/Assets/.gitkeep old mode 100755 new mode 100644 similarity index 100% rename from packages/image_picker/image_picker/ios/Assets/.gitkeep rename to packages/google_sign_in/google_sign_in_ios/ios/Assets/.gitkeep diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.h b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.h similarity index 100% rename from packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.h rename to packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.h diff --git a/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m new file mode 100644 index 000000000000..7beb604aaee3 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m @@ -0,0 +1,319 @@ +// 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 "FLTGoogleSignInPlugin.h" +#import "FLTGoogleSignInPlugin_Test.h" + +#import + +// The key within `GoogleService-Info.plist` used to hold the application's +// client id. See https://developers.google.com/identity/sign-in/ios/start +// for more info. +static NSString *const kClientIdKey = @"CLIENT_ID"; + +static NSString *const kServerClientIdKey = @"SERVER_CLIENT_ID"; + +static NSDictionary *loadGoogleServiceInfo() { + NSString *plistPath = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" + ofType:@"plist"]; + if (plistPath) { + return [[NSDictionary alloc] initWithContentsOfFile:plistPath]; + } + return nil; +} + +// These error codes must match with ones declared on Android and Dart sides. +static NSString *const kErrorReasonSignInRequired = @"sign_in_required"; +static NSString *const kErrorReasonSignInCanceled = @"sign_in_canceled"; +static NSString *const kErrorReasonNetworkError = @"network_error"; +static NSString *const kErrorReasonSignInFailed = @"sign_in_failed"; + +static FlutterError *getFlutterError(NSError *error) { + NSString *errorCode; + if (error.code == kGIDSignInErrorCodeHasNoAuthInKeychain) { + errorCode = kErrorReasonSignInRequired; + } else if (error.code == kGIDSignInErrorCodeCanceled) { + errorCode = kErrorReasonSignInCanceled; + } else if ([error.domain isEqualToString:NSURLErrorDomain]) { + errorCode = kErrorReasonNetworkError; + } else { + errorCode = kErrorReasonSignInFailed; + } + return [FlutterError errorWithCode:errorCode + message:error.domain + details:error.localizedDescription]; +} + +@interface FLTGoogleSignInPlugin () + +// Configuration wrapping Google Cloud Console, Google Apps, OpenID, +// and other initialization metadata. +@property(strong) GIDConfiguration *configuration; + +// Permissions requested during at sign in "init" method call +// unioned with scopes requested later with incremental authorization +// "requestScopes" method call. +// The "email" and "profile" base scopes are always implicitly requested. +@property(copy) NSSet *requestedScopes; + +// Instance used to manage Google Sign In authentication including +// sign in, sign out, and requesting additional scopes. +@property(strong, readonly) GIDSignIn *signIn; + +// The contents of GoogleService-Info.plist, if it exists. +@property(strong, nullable) NSDictionary *googleServiceProperties; + +// Redeclared as not a designated initializer. +- (instancetype)init; + +@end + +@implementation FLTGoogleSignInPlugin + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/google_sign_in_ios" + binaryMessenger:[registrar messenger]]; + FLTGoogleSignInPlugin *instance = [[FLTGoogleSignInPlugin alloc] init]; + [registrar addApplicationDelegate:instance]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (instancetype)init { + return [self initWithSignIn:GIDSignIn.sharedInstance]; +} + +- (instancetype)initWithSignIn:(GIDSignIn *)signIn { + return [self initWithSignIn:signIn withGoogleServiceProperties:loadGoogleServiceInfo()]; +} + +- (instancetype)initWithSignIn:(GIDSignIn *)signIn + withGoogleServiceProperties:(nullable NSDictionary *)googleServiceProperties { + self = [super init]; + if (self) { + _signIn = signIn; + _googleServiceProperties = googleServiceProperties; + + // On the iOS simulator, we get "Broken pipe" errors after sign-in for some + // unknown reason. We can avoid crashing the app by ignoring them. + signal(SIGPIPE, SIG_IGN); + _requestedScopes = [[NSSet alloc] init]; + } + return self; +} + +#pragma mark - protocol + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([call.method isEqualToString:@"init"]) { + GIDConfiguration *configuration = + [self configurationWithClientIdArgument:call.arguments[@"clientId"] + serverClientIdArgument:call.arguments[@"serverClientId"] + hostedDomainArgument:call.arguments[@"hostedDomain"]]; + if (configuration != nil) { + if ([call.arguments[@"scopes"] isKindOfClass:[NSArray class]]) { + self.requestedScopes = [NSSet setWithArray:call.arguments[@"scopes"]]; + } + self.configuration = configuration; + result(nil); + } else { + result([FlutterError errorWithCode:@"missing-config" + message:@"GoogleService-Info.plist file not found and clientId " + @"was not provided programmatically." + details:nil]); + } + } else if ([call.method isEqualToString:@"signInSilently"]) { + [self.signIn restorePreviousSignInWithCallback:^(GIDGoogleUser *user, NSError *error) { + [self didSignInForUser:user result:result withError:error]; + }]; + } else if ([call.method isEqualToString:@"isSignedIn"]) { + result(@([self.signIn hasPreviousSignIn])); + } else if ([call.method isEqualToString:@"signIn"]) { + @try { + GIDConfiguration *configuration = self.configuration + ?: [self configurationWithClientIdArgument:nil + serverClientIdArgument:nil + hostedDomainArgument:nil]; + [self.signIn signInWithConfiguration:configuration + presentingViewController:[self topViewController] + hint:nil + additionalScopes:self.requestedScopes.allObjects + callback:^(GIDGoogleUser *user, NSError *error) { + [self didSignInForUser:user result:result withError:error]; + }]; + } @catch (NSException *e) { + result([FlutterError errorWithCode:@"google_sign_in" message:e.reason details:e.name]); + [e raise]; + } + } else if ([call.method isEqualToString:@"getTokens"]) { + GIDGoogleUser *currentUser = self.signIn.currentUser; + GIDAuthentication *auth = currentUser.authentication; + [auth doWithFreshTokens:^void(GIDAuthentication *authentication, NSError *error) { + result(error != nil ? getFlutterError(error) : @{ + @"idToken" : authentication.idToken, + @"accessToken" : authentication.accessToken, + }); + }]; + } else if ([call.method isEqualToString:@"signOut"]) { + [self.signIn signOut]; + result(nil); + } else if ([call.method isEqualToString:@"disconnect"]) { + [self.signIn disconnectWithCallback:^(NSError *error) { + [self respondWithAccount:@{} result:result error:nil]; + }]; + } else if ([call.method isEqualToString:@"requestScopes"]) { + id scopeArgument = call.arguments[@"scopes"]; + if ([scopeArgument isKindOfClass:[NSArray class]]) { + self.requestedScopes = [self.requestedScopes setByAddingObjectsFromArray:scopeArgument]; + } + NSSet *requestedScopes = self.requestedScopes; + + @try { + [self.signIn addScopes:requestedScopes.allObjects + presentingViewController:[self topViewController] + callback:^(GIDGoogleUser *addedScopeUser, NSError *addedScopeError) { + if ([addedScopeError.domain isEqualToString:kGIDSignInErrorDomain] && + addedScopeError.code == kGIDSignInErrorCodeNoCurrentUser) { + result([FlutterError errorWithCode:@"sign_in_required" + message:@"No account to grant scopes." + details:nil]); + } else if ([addedScopeError.domain + isEqualToString:kGIDSignInErrorDomain] && + addedScopeError.code == + kGIDSignInErrorCodeScopesAlreadyGranted) { + // Scopes already granted, report success. + result(@YES); + } else if (addedScopeUser == nil) { + result(@NO); + } else { + NSSet *grantedScopes = + [NSSet setWithArray:addedScopeUser.grantedScopes]; + BOOL granted = [requestedScopes isSubsetOfSet:grantedScopes]; + result(@(granted)); + } + }]; + } @catch (NSException *e) { + result([FlutterError errorWithCode:@"request_scopes" message:e.reason details:e.name]); + } + } else { + result(FlutterMethodNotImplemented); + } +} + +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { + return [self.signIn handleURL:url]; +} + +#pragma mark - protocol + +- (void)signIn:(GIDSignIn *)signIn presentViewController:(UIViewController *)viewController { + UIViewController *rootViewController = + [UIApplication sharedApplication].delegate.window.rootViewController; + [rootViewController presentViewController:viewController animated:YES completion:nil]; +} + +- (void)signIn:(GIDSignIn *)signIn dismissViewController:(UIViewController *)viewController { + [viewController dismissViewControllerAnimated:YES completion:nil]; +} + +#pragma mark - private methods + +/// @return @c nil if GoogleService-Info.plist not found and clientId is not provided. +- (GIDConfiguration *)configurationWithClientIdArgument:(id)clientIDArg + serverClientIdArgument:(id)serverClientIDArg + hostedDomainArgument:(id)hostedDomainArg { + NSString *clientID; + BOOL hasDynamicClientId = [clientIDArg isKindOfClass:[NSString class]]; + if (hasDynamicClientId) { + clientID = clientIDArg; + } else if (self.googleServiceProperties) { + clientID = self.googleServiceProperties[kClientIdKey]; + } else { + // We couldn't resolve a clientId, without which we cannot create a GIDConfiguration. + return nil; + } + + BOOL hasDynamicServerClientId = [serverClientIDArg isKindOfClass:[NSString class]]; + NSString *serverClientID = hasDynamicServerClientId + ? serverClientIDArg + : self.googleServiceProperties[kServerClientIdKey]; + + NSString *hostedDomain = nil; + if (hostedDomainArg != [NSNull null]) { + hostedDomain = hostedDomainArg; + } + return [[GIDConfiguration alloc] initWithClientID:clientID + serverClientID:serverClientID + hostedDomain:hostedDomain + openIDRealm:nil]; +} + +- (void)didSignInForUser:(GIDGoogleUser *)user + result:(FlutterResult)result + withError:(NSError *)error { + if (error != nil) { + // Forward all errors and let Dart side decide how to handle. + [self respondWithAccount:nil result:result error:error]; + } else { + NSURL *photoUrl; + if (user.profile.hasImage) { + // Placeholder that will be replaced by on the Dart side based on screen size. + photoUrl = [user.profile imageURLWithDimension:1337]; + } + [self respondWithAccount:@{ + @"displayName" : user.profile.name ?: [NSNull null], + @"email" : user.profile.email ?: [NSNull null], + @"id" : user.userID ?: [NSNull null], + @"photoUrl" : [photoUrl absoluteString] ?: [NSNull null], + @"serverAuthCode" : user.serverAuthCode ?: [NSNull null] + } + result:result + error:nil]; + } +} + +- (void)respondWithAccount:(NSDictionary *)account + result:(FlutterResult)result + error:(NSError *)error { + result(error != nil ? getFlutterError(error) : account); +} + +- (UIViewController *)topViewController { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // TODO(stuartmorgan) Provide a non-deprecated codepath. See + // https://github.com/flutter/flutter/issues/104117 + return [self topViewControllerFromViewController:[UIApplication sharedApplication] + .keyWindow.rootViewController]; +#pragma clang diagnostic pop +} + +/** + * This method recursively iterate through the view hierarchy + * to return the top most view controller. + * + * It supports the following scenarios: + * + * - The view controller is presenting another view. + * - The view controller is a UINavigationController. + * - The view controller is a UITabBarController. + * + * @return The top most view controller. + */ +- (UIViewController *)topViewControllerFromViewController:(UIViewController *)viewController { + if ([viewController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navigationController = (UINavigationController *)viewController; + return [self + topViewControllerFromViewController:[navigationController.viewControllers lastObject]]; + } + if ([viewController isKindOfClass:[UITabBarController class]]) { + UITabBarController *tabController = (UITabBarController *)viewController; + return [self topViewControllerFromViewController:tabController.selectedViewController]; + } + if (viewController.presentedViewController) { + return [self topViewControllerFromViewController:viewController.presentedViewController]; + } + return viewController; +} +@end diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.modulemap similarity index 55% rename from packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap rename to packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.modulemap index 271f509e7fd7..31e30d93c582 100644 --- a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap +++ b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.modulemap @@ -1,5 +1,5 @@ -framework module google_sign_in { - umbrella header "google_sign_in-umbrella.h" +framework module google_sign_in_ios { + umbrella header "google_sign_in_ios-umbrella.h" export * module * { export * } diff --git a/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin_Test.h b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin_Test.h new file mode 100644 index 000000000000..17ddb7f616bc --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin_Test.h @@ -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. + +// This header is available in the Test module. Import via "@import google_sign_in.Test;" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class GIDSignIn; + +/// Methods exposed for unit testing. +@interface FLTGoogleSignInPlugin () + +/// Inject @c GIDSignIn for testing. +- (instancetype)initWithSignIn:(GIDSignIn *)signIn; + +/// Inject @c GIDSignIn and @c googleServiceProperties for testing. +- (instancetype)initWithSignIn:(GIDSignIn *)signIn + withGoogleServiceProperties:(nullable NSDictionary *)googleServiceProperties + NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h b/packages/google_sign_in/google_sign_in_ios/ios/Classes/google_sign_in_ios-umbrella.h similarity index 85% rename from packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h rename to packages/google_sign_in/google_sign_in_ios/ios/Classes/google_sign_in_ios-umbrella.h index 343c390f1782..23b7e992a5cd 100644 --- a/packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h +++ b/packages/google_sign_in/google_sign_in_ios/ios/Classes/google_sign_in_ios-umbrella.h @@ -3,7 +3,7 @@ // found in the LICENSE file. #import -#import +#import FOUNDATION_EXPORT double google_sign_inVersionNumber; FOUNDATION_EXPORT const unsigned char google_sign_inVersionString[]; diff --git a/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec b/packages/google_sign_in/google_sign_in_ios/ios/google_sign_in_ios.podspec similarity index 73% rename from packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec rename to packages/google_sign_in/google_sign_in_ios/ios/google_sign_in_ios.podspec index 19ea753520a7..4e307098fd6d 100644 --- a/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec +++ b/packages/google_sign_in/google_sign_in_ios/ios/google_sign_in_ios.podspec @@ -2,7 +2,7 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| - s.name = 'google_sign_in' + s.name = 'google_sign_in_ios' s.version = '0.0.1' s.summary = 'Google Sign-In plugin for Flutter' s.description = <<-DESC @@ -11,16 +11,13 @@ Enables Google Sign-In in Flutter apps. s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/google_sign_in' s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/google_sign_in' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in_ios' } s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' s.module_map = 'Classes/FLTGoogleSignInPlugin.modulemap' s.dependency 'Flutter' - s.dependency 'GoogleSignIn', '~> 5.0' + s.dependency 'GoogleSignIn', '~> 6.2' s.static_framework = true - s.platform = :ios, '9.0' - - # GoogleSignIn ~> 5.0 does not support arm64 simulators. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart new file mode 100644 index 000000000000..07407eaf5236 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.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/foundation.dart' show visibleForTesting; +import 'package:flutter/services.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +import 'src/utils.dart'; + +/// iOS implementation of [GoogleSignInPlatform]. +class GoogleSignInIOS extends GoogleSignInPlatform { + /// 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 + MethodChannel channel = + const MethodChannel('plugins.flutter.io/google_sign_in_ios'); + + /// Registers this class as the default instance of [GoogleSignInPlatform]. + static void registerWith() { + GoogleSignInPlatform.instance = GoogleSignInIOS(); + } + + @override + Future init({ + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String? hostedDomain, + String? clientId, + }) { + return initWithParams(SignInInitParameters( + signInOption: signInOption, + scopes: scopes, + hostedDomain: hostedDomain, + clientId: clientId, + )); + } + + @override + Future initWithParams(SignInInitParameters params) { + if (params.signInOption == SignInOption.games) { + throw PlatformException( + code: 'unsupported-options', + message: 'Games sign in is not supported on iOS'); + } + return channel.invokeMethod('init', { + 'scopes': params.scopes, + 'hostedDomain': params.hostedDomain, + 'clientId': params.clientId, + }); + } + + @override + Future signInSilently() { + return channel + .invokeMapMethod('signInSilently') + .then(getUserDataFromMap); + } + + @override + Future signIn() { + return channel + .invokeMapMethod('signIn') + .then(getUserDataFromMap); + } + + @override + Future getTokens( + {required String email, bool? shouldRecoverAuth = true}) { + return channel + .invokeMapMethod('getTokens', { + 'email': email, + 'shouldRecoverAuth': shouldRecoverAuth, + }).then((Map? result) => getTokenDataFromMap(result!)); + } + + @override + Future signOut() { + return channel.invokeMapMethod('signOut'); + } + + @override + Future disconnect() { + return channel.invokeMapMethod('disconnect'); + } + + @override + Future isSignedIn() async { + return (await channel.invokeMethod('isSignedIn'))!; + } + + @override + Future clearAuthCache({String? token}) async { + // There's nothing to be done here on iOS since the expired/invalid + // tokens are refreshed automatically by getTokens. + } + + @override + Future requestScopes(List scopes) async { + return (await channel.invokeMethod( + 'requestScopes', + >{'scopes': scopes}, + ))!; + } +} diff --git a/packages/google_sign_in/google_sign_in_ios/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_ios/lib/src/utils.dart new file mode 100644 index 000000000000..5cd7c20b829a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/lib/src/utils.dart @@ -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 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +/// Converts user data coming from native code into the proper platform interface type. +GoogleSignInUserData? getUserDataFromMap(Map? data) { + if (data == null) { + return null; + } + return GoogleSignInUserData( + email: data['email']! as String, + id: data['id']! as String, + displayName: data['displayName'] as String?, + photoUrl: data['photoUrl'] as String?, + idToken: data['idToken'] as String?, + serverAuthCode: data['serverAuthCode'] as String?); +} + +/// Converts token data coming from native code into the proper platform interface type. +GoogleSignInTokenData getTokenDataFromMap(Map data) { + return GoogleSignInTokenData( + idToken: data['idToken'] as String?, + accessToken: data['accessToken'] as String?, + serverAuthCode: data['serverAuthCode'] as String?, + ); +} diff --git a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml new file mode 100644 index 000000000000..24c08cdaa674 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml @@ -0,0 +1,36 @@ +name: google_sign_in_ios +description: iOS implementation of the google_sign_in plugin. +repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in_ios +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 +version: 5.5.0 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: google_sign_in + platforms: + ios: + dartPluginClass: GoogleSignInIOS + pluginClass: FLTGoogleSignInPlugin + +dependencies: + flutter: + sdk: flutter + google_sign_in_platform_interface: ^2.2.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +# The example deliberately includes limited-use secrets. +false_secrets: + - /example/ios/Runner/GoogleService-Info.plist + - /example/ios/RunnerTests/GoogleSignInTests.m + - /example/lib/main.dart 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 new file mode 100644 index 000000000000..ace65092f61d --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart @@ -0,0 +1,161 @@ +// 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'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_ios/google_sign_in_ios.dart'; +import 'package:google_sign_in_ios/src/utils.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +const Map kUserData = { + 'email': 'john.doe@gmail.com', + 'id': '8162538176523816253123', + 'photoUrl': 'https://lh5.googleusercontent.com/photo.jpg', + 'displayName': 'John Doe', + 'idToken': '123', + 'serverAuthCode': '789', +}; + +const Map kTokenData = { + 'idToken': '123', + 'accessToken': '456', + 'serverAuthCode': '789', +}; + +const Map kDefaultResponses = { + 'init': null, + 'signInSilently': kUserData, + 'signIn': kUserData, + 'signOut': null, + 'disconnect': null, + 'isSignedIn': true, + 'getTokens': kTokenData, + 'requestScopes': true, +}; + +final GoogleSignInUserData? kUser = getUserDataFromMap(kUserData); +final GoogleSignInTokenData kToken = + getTokenDataFromMap(kTokenData as Map); + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final GoogleSignInIOS googleSignIn = GoogleSignInIOS(); + final MethodChannel channel = googleSignIn.channel; + + late List log; + late Map + responses; // Some tests mutate some kDefaultResponses + + 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); + }); + }); + + test('registered instance', () { + GoogleSignInIOS.registerWith(); + expect(GoogleSignInPlatform.instance, isA()); + }); + + test('init throws for SignInOptions.games', () async { + expect( + () => googleSignIn.init( + hostedDomain: 'example.com', + signInOption: SignInOption.games, + clientId: 'fakeClientId'), + throwsA(isInstanceOf().having( + (PlatformException e) => e.code, 'code', 'unsupported-options'))); + }); + + test('signInSilently transforms platform data to GoogleSignInUserData', + () async { + final dynamic response = await googleSignIn.signInSilently(); + expect(response, kUser); + }); + test('signInSilently Exceptions -> throws', () async { + responses['signInSilently'] = Exception('Not a user'); + expect(googleSignIn.signInSilently(), + throwsA(isInstanceOf())); + }); + + test('signIn transforms platform data to GoogleSignInUserData', () async { + final dynamic response = await googleSignIn.signIn(); + expect(response, kUser); + }); + test('signIn Exceptions -> throws', () async { + responses['signIn'] = Exception('Not a user'); + expect(googleSignIn.signIn(), throwsA(isInstanceOf())); + }); + + test('getTokens transforms platform data to GoogleSignInTokenData', () async { + final dynamic response = await googleSignIn.getTokens( + email: 'example@example.com', shouldRecoverAuth: false); + expect(response, kToken); + expect( + log[0], + isMethodCall('getTokens', arguments: { + 'email': 'example@example.com', + 'shouldRecoverAuth': false, + })); + }); + + test('clearAuthCache is a no-op', () async { + await googleSignIn.clearAuthCache(token: 'abc'); + expect(log.isEmpty, true); + }); + + test('Other functions pass through arguments to the channel', () async { + final Map tests = { + () { + googleSignIn.init( + hostedDomain: 'example.com', + scopes: ['two', 'scopes'], + clientId: 'fakeClientId'); + }: isMethodCall('init', arguments: { + 'hostedDomain': 'example.com', + 'scopes': ['two', 'scopes'], + 'clientId': 'fakeClientId', + }), + () { + googleSignIn.initWithParams(const SignInInitParameters( + hostedDomain: 'example.com', + scopes: ['two', 'scopes'], + clientId: 'fakeClientId')); + }: isMethodCall('init', arguments: { + 'hostedDomain': 'example.com', + 'scopes': ['two', 'scopes'], + 'clientId': 'fakeClientId', + }), + () { + googleSignIn.getTokens( + email: 'example@example.com', shouldRecoverAuth: false); + }: isMethodCall('getTokens', arguments: { + 'email': 'example@example.com', + 'shouldRecoverAuth': false, + }), + () { + googleSignIn.requestScopes(['newScope', 'anotherScope']); + }: isMethodCall('requestScopes', arguments: { + 'scopes': ['newScope', 'anotherScope'], + }), + googleSignIn.signOut: isMethodCall('signOut', arguments: null), + googleSignIn.disconnect: isMethodCall('disconnect', arguments: null), + googleSignIn.isSignedIn: isMethodCall('isSignedIn', arguments: null), + }; + + for (final Function f in tests.keys) { + f(); + } + + expect(log, tests.values); + }); +} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/AUTHORS b/packages/google_sign_in/google_sign_in_platform_interface/AUTHORS index 493a0b4ef9c2..35d24a5ae0b5 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/AUTHORS +++ b/packages/google_sign_in/google_sign_in_platform_interface/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Twin Sun, LLC 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 66fdb3e72a56..01d54b23dae0 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md @@ -1,3 +1,22 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 2.3.0 + +* Adopts `plugin_platform_interface`. As a result, `isMock` is deprecated in + favor of the now-standard `MockPlatformInterfaceMixin`. + +## 2.2.0 + +* Adds support for the `serverClientId` parameter. + +## 2.1.3 + +* Enables mocking models by changing overridden operator == parameter type from `dynamic` to `Object`. +* Removes unnecessary imports. +* Adds `SignInInitParameters` class to hold all sign in params, including the new `forceCodeForRefreshToken`. + ## 2.1.2 * Internal code cleanup for stricter analysis options. diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart index 50f261bfa578..64fc88d4866f 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'src/method_channel_google_sign_in.dart'; import 'src/types.dart'; @@ -20,13 +21,19 @@ export 'src/types.dart'; /// ensures that the subclass will get the default implementation, while /// platform implementations that `implements` this interface will be broken by /// newly added [GoogleSignInPlatform] methods. -abstract class GoogleSignInPlatform { +abstract class GoogleSignInPlatform extends PlatformInterface { + /// Constructs a GoogleSignInPlatform. + GoogleSignInPlatform() : super(token: _token); + + static final Object _token = Object(); + /// Only mock implementations should set this to `true`. /// /// Mockito mocks implement this class with `implements` which is forbidden /// (see class docs). This property provides a backdoor for mocks to skip the /// verification that the class isn't implemented with `implements`. @visibleForTesting + @Deprecated('Use MockPlatformInterfaceMixin instead') bool get isMock => false; /// The default instance of [GoogleSignInPlatform] to use. @@ -44,27 +51,12 @@ abstract class GoogleSignInPlatform { // https://github.com/flutter/flutter/issues/43368 static set instance(GoogleSignInPlatform instance) { if (!instance.isMock) { - try { - instance._verifyProvidesDefaultImplementations(); - } on NoSuchMethodError catch (_) { - throw AssertionError( - 'Platform interfaces must not be implemented with `implements`'); - } + PlatformInterface.verify(instance, _token); } _instance = instance; } - /// This method ensures that [GoogleSignInPlatform] isn't implemented with `implements`. - /// - /// See class docs for more details on why using `implements` to implement - /// [GoogleSignInPlatform] is forbidden. - /// - /// This private method is called by the [instance] setter, which should fail - /// if the provided instance is a class implemented with `implements`. - void _verifyProvidesDefaultImplementations() {} - - /// Initializes the plugin. You must call this method before calling other - /// methods. + /// Initializes the plugin. Deprecated: call [initWithParams] instead. /// /// The [hostedDomain] argument specifies a hosted domain restriction. By /// setting this, sign in will be restricted to accounts of the user in the @@ -89,6 +81,21 @@ abstract class GoogleSignInPlatform { throw UnimplementedError('init() has not been implemented.'); } + /// Initializes the plugin with specified [params]. You must call this method + /// before calling other methods. + /// + /// See: + /// + /// * [SignInInitParameters] + Future initWithParams(SignInInitParameters params) async { + await init( + scopes: params.scopes, + signInOption: params.signInOption, + hostedDomain: params.hostedDomain, + clientId: params.clientId, + ); + } + /// Attempts to reuse pre-existing credentials to sign in again, without user interaction. Future signInSilently() async { throw UnimplementedError('signInSilently() has not been implemented.'); diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart index 1abda09fa99f..c3b158dd8450 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart @@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/services.dart'; import '../google_sign_in_platform_interface.dart'; -import 'types.dart'; import 'utils.dart'; /// An implementation of [GoogleSignInPlatform] that uses method channels. @@ -26,11 +25,22 @@ class MethodChannelGoogleSignIn extends GoogleSignInPlatform { String? hostedDomain, String? clientId, }) { + return initWithParams(SignInInitParameters( + scopes: scopes, + signInOption: signInOption, + hostedDomain: hostedDomain, + clientId: clientId)); + } + + @override + Future initWithParams(SignInInitParameters params) { return channel.invokeMethod('init', { - 'signInOption': signInOption.toString(), - 'scopes': scopes, - 'hostedDomain': hostedDomain, - 'clientId': clientId, + 'signInOption': params.signInOption.toString(), + 'scopes': params.scopes, + 'hostedDomain': params.hostedDomain, + 'clientId': params.clientId, + 'serverClientId': params.serverClientId, + 'forceCodeForRefreshToken': params.forceCodeForRefreshToken, }); } diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart index bc50a1d2516d..422fe807253d 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/widgets.dart'; import 'package:quiver/core.dart'; /// Default configuration options to use when signing in. @@ -22,6 +23,64 @@ enum SignInOption { games } +/// The parameters to use when initializing the sign in process. +/// +/// See: +/// https://developers.google.com/identity/sign-in/web/reference#gapiauth2initparams +@immutable +class SignInInitParameters { + /// The parameters to use when initializing the sign in process. + const SignInInitParameters({ + this.scopes = const [], + this.signInOption = SignInOption.standard, + this.hostedDomain, + this.clientId, + this.serverClientId, + this.forceCodeForRefreshToken = false, + }); + + /// The list of OAuth scope codes to request when signing in. + final List scopes; + + /// The user experience to use when signing in. [SignInOption.games] is + /// only supported on Android. + final SignInOption signInOption; + + /// Restricts sign in to accounts of the user in the specified domain. + /// By default, the list of accounts will not be restricted. + final String? hostedDomain; + + /// The OAuth client ID of the app. + /// + /// The default is null, which means that the client ID will be sourced from a + /// configuration file, if required on the current platform. A value specified + /// here takes precedence over a value specified in a configuration file. + /// See also: + /// + /// * [Platform Integration](https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in#platform-integration), + /// where you can find the details about the configuration files. + final String? clientId; + + /// The OAuth client ID of the backend server. + /// + /// The default is null, which means that the server client ID will be sourced + /// from a configuration file, if available and supported on the current + /// platform. A value specified here takes precedence over a value specified + /// in a configuration file. + /// + /// See also: + /// + /// * [Platform Integration](https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in#platform-integration), + /// where you can find the details about the configuration files. + final String? serverClientId; + + /// If true, ensures the authorization code can be exchanged for an access + /// token. + /// + /// This is only used on Android. + final bool forceCodeForRefreshToken; +} + /// Holds information about the signed in user. class GoogleSignInUserData { /// Uses the given data to construct an instance. @@ -79,7 +138,7 @@ class GoogleSignInUserData { @override // TODO(stuartmorgan): Make this class immutable in the next breaking change. // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (identical(this, other)) { return true; } @@ -122,7 +181,7 @@ class GoogleSignInTokenData { @override // TODO(stuartmorgan): Make this class immutable in the next breaking change. // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (identical(this, other)) { return true; } 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 a5bbaedd51e7..0902069364ce 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 @@ -4,15 +4,16 @@ repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.1.2 +version: 2.3.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" dependencies: flutter: sdk: flutter + plugin_platform_interface: ^2.1.0 quiver: ^3.0.0 dev_dependencies: diff --git a/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart b/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart index 78e57a3eb2ac..057f13cb26f5 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart @@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; void main() { // Store the initial instance before any tests change it. @@ -18,7 +19,14 @@ void main() { test('Cannot be implemented with `implements`', () { expect(() { GoogleSignInPlatform.instance = ImplementsGoogleSignInPlatform(); - }, throwsA(isA())); + // In versions of `package:plugin_platform_interface` prior to fixing + // https://github.com/flutter/flutter/issues/109339, an attempt to + // implement a platform interface using `implements` would sometimes + // throw a `NoSuchMethodError` and other times throw an + // `AssertionError`. After the issue is fixed, an `AssertionError` will + // always be thrown. For the purpose of this test, we don't really care + // what exception is thrown, so just allow any exception. + }, throwsA(anything)); }); test('Can be extended', () { @@ -26,16 +34,65 @@ void main() { }); test('Can be mocked with `implements`', () { - GoogleSignInPlatform.instance = ImplementsWithIsMock(); + GoogleSignInPlatform.instance = ModernMockImplementation(); + }); + + test('still supports legacy isMock', () { + GoogleSignInPlatform.instance = LegacyIsMockImplementation(); + }); + }); + + group('GoogleSignInTokenData', () { + test('can be compared by == operator', () { + final GoogleSignInTokenData firstInstance = GoogleSignInTokenData( + accessToken: 'accessToken', + idToken: 'idToken', + serverAuthCode: 'serverAuthCode', + ); + final GoogleSignInTokenData secondInstance = GoogleSignInTokenData( + accessToken: 'accessToken', + idToken: 'idToken', + serverAuthCode: 'serverAuthCode', + ); + expect(firstInstance == secondInstance, isTrue); + }); + }); + + group('GoogleSignInUserData', () { + test('can be compared by == operator', () { + final GoogleSignInUserData firstInstance = GoogleSignInUserData( + email: 'email', + id: 'id', + displayName: 'displayName', + photoUrl: 'photoUrl', + idToken: 'idToken', + serverAuthCode: 'serverAuthCode', + ); + final GoogleSignInUserData secondInstance = GoogleSignInUserData( + email: 'email', + id: 'id', + displayName: 'displayName', + photoUrl: 'photoUrl', + idToken: 'idToken', + serverAuthCode: 'serverAuthCode', + ); + expect(firstInstance == secondInstance, isTrue); }); }); } -class ImplementsWithIsMock extends Mock implements GoogleSignInPlatform { +class LegacyIsMockImplementation extends Mock implements GoogleSignInPlatform { @override bool get isMock => true; } +class ModernMockImplementation extends Mock + with MockPlatformInterfaceMixin + implements GoogleSignInPlatform { + @override + bool get isMock => false; +} + class ImplementsGoogleSignInPlatform extends Mock implements GoogleSignInPlatform {} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart index a1d83c3f05e6..944ad3419b8e 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 @@ -5,7 +5,6 @@ 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_platform_interface/src/types.dart'; import 'package:google_sign_in_platform_interface/src/utils.dart'; const Map kUserData = { @@ -108,6 +107,8 @@ void main() { 'scopes': ['two', 'scopes'], 'signInOption': 'SignInOption.games', 'clientId': 'fakeClientId', + 'serverClientId': null, + 'forceCodeForRefreshToken': false, }), () { googleSignIn.getTokens( @@ -137,5 +138,25 @@ void main() { expect(log, tests.values); }); + + test('initWithParams passes through arguments to the channel', () async { + await googleSignIn.initWithParams(const SignInInitParameters( + hostedDomain: 'example.com', + scopes: ['two', 'scopes'], + signInOption: SignInOption.games, + clientId: 'fakeClientId', + serverClientId: 'fakeServerClientId', + forceCodeForRefreshToken: true)); + expect(log, [ + isMethodCall('init', arguments: { + 'hostedDomain': 'example.com', + 'scopes': ['two', 'scopes'], + 'signInOption': 'SignInOption.games', + 'clientId': 'fakeClientId', + 'serverClientId': 'fakeServerClientId', + 'forceCodeForRefreshToken': true, + }), + ]); + }); }); } 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 c1a1606d3a2f..2816e7284b30 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,32 @@ +## NEXT + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. +* Updates minimum Flutter version to 2.10. + +## 0.10.2 + +* Migrates to new platform-interface `initWithParams` method. +* Throws when unsupported `serverClientId` option is provided. + +## 0.10.1+3 + +* Updates references to the obsolete master branch. + +## 0.10.1+2 + +* Minor fixes for new analysis options. + +## 0.10.1+1 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.10.1 + +* Updates minimum Flutter version to 2.8. +* Passes `plugin_name` to Google Sign-In's `init` method so new applications can + continue using this plugin after April 30th 2022. Issue [#88084](https://github.com/flutter/flutter/issues/88084). + ## 0.10.0+5 * Internal code cleanup for stricter analysis options. 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 4ee1a2956b45..7c02379808da 100644 --- a/packages/google_sign_in/google_sign_in_web/README.md +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -37,7 +37,7 @@ Normally `flutter run` starts in a random port. In the case where you need to de You can tell `flutter run` to listen for requests in a specific host and port with the following: -``` +```sh flutter run -d chrome --web-hostname localhost --web-port 7357 ``` @@ -63,8 +63,11 @@ GoogleSignIn _googleSignIn = GoogleSignIn( ], ); ``` + [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. + You can now use the `GoogleSignIn` class to authenticate in your Dart code, e.g. ```dart @@ -79,19 +82,19 @@ Future _handleSignIn() async { ## Example -Find the example wiring in the [Google sign-in example application](https://github.com/flutter/plugins/blob/master/packages/google_sign_in/google_sign_in/example/lib/main.dart). +Find the example wiring in the [Google sign-in example application](https://github.com/flutter/plugins/blob/main/packages/google_sign_in/google_sign_in/example/lib/main.dart). ## API details -See the [google_sign_in.dart](https://github.com/flutter/plugins/blob/master/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart) for more 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. ## Contributions and Testing Tests are crucial for contributions to this package. All new contributions should be reasonably tested. -**Check the [`test/README.md` file](https://github.com/flutter/plugins/blob/master/packages/google_sign_in/google_sign_in_web/test/README.md)** for more information on how to run tests on this package. +**Check the [`test/README.md` file](https://github.com/flutter/plugins/blob/main/packages/google_sign_in/google_sign_in_web/test/README.md)** for more information on how to run tests on this package. -Contributions to this package are welcome. Read the [Contributing to Flutter Plugins](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md) guide to get started. +Contributions to this package are welcome. Read the [Contributing to Flutter Plugins](https://github.com/flutter/plugins/blob/main/CONTRIBUTING.md) guide to get started. ## Issues and feedback diff --git a/packages/google_sign_in/google_sign_in_web/example/README.md b/packages/google_sign_in/google_sign_in_web/example/README.md index 8a6e74b107ea..0e51ae5ecbd2 100644 --- a/packages/google_sign_in/google_sign_in_web/example/README.md +++ b/packages/google_sign_in/google_sign_in_web/example/README.md @@ -1,4 +1,14 @@ -# Testing +# 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. + +## Testing This package uses `package:integration_test` to run its tests in a web browser. @@ -6,4 +16,4 @@ See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Te in the Flutter wiki for instructions to setup and run the tests in this package. Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) -for more info. \ No newline at end of file +for more info. 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 new file mode 100644 index 000000000000..5dada90397fa --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_legacy_init_test.dart @@ -0,0 +1,223 @@ +// 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 index 2af9476dbb33..3e803b83fa0c 100644 --- 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 @@ -2,6 +2,8 @@ // 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'; @@ -28,32 +30,31 @@ void main() { late GoogleSignInPlugin plugin; - group('plugin.init() throws a catchable exception', () { + 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('init throws PlatformException', (WidgetTester tester) async { + testWidgets('throws PlatformException', (WidgetTester tester) async { await expectLater( - plugin.init( + plugin.initWithParams(const SignInInitParameters( hostedDomain: 'foo', scopes: ['some', 'scope'], clientId: '1234', - ), + )), throwsA(isA())); }); - testWidgets('init forwards error code from JS', - (WidgetTester tester) async { + testWidgets('forwards error code from JS', (WidgetTester tester) async { try { - await plugin.init( + await plugin.initWithParams(const SignInInitParameters( hostedDomain: 'foo', scopes: ['some', 'scope'], clientId: '1234', - ); - fail('plugin.init should have thrown an exception!'); + )); + fail('plugin.initWithParams should have thrown an exception!'); } catch (e) { final String code = js_util.getProperty(e, 'code'); expect(code, 'idpiframe_initialization_failed'); @@ -61,16 +62,17 @@ void main() { }); }); - group('other methods also throw catchable exceptions on init fail', () { - // This function ensures that init gets called, but for some reason, we - // ignored that it has thrown stuff... - Future _discardInit() async { + 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.init( + await plugin.initWithParams(const SignInInitParameters( hostedDomain: 'foo', scopes: ['some', 'scope'], clientId: '1234', - ); + )); } catch (e) { // Noop so we can call other stuff } @@ -82,23 +84,23 @@ void main() { }); testWidgets('signInSilently throws', (WidgetTester tester) async { - await _discardInit(); + await discardInit(); await expectLater( plugin.signInSilently(), throwsA(isA())); }); testWidgets('signIn throws', (WidgetTester tester) async { - await _discardInit(); + await discardInit(); await expectLater(plugin.signIn(), throwsA(isA())); }); testWidgets('getTokens throws', (WidgetTester tester) async { - await _discardInit(); + await discardInit(); await expectLater(plugin.getTokens(email: 'test@example.com'), throwsA(isA())); }); testWidgets('requestScopes', (WidgetTester tester) async { - await _discardInit(); + await discardInit(); await expectLater(plugin.requestScopes(['newScope']), throwsA(isA())); }); @@ -112,27 +114,58 @@ void main() { }); testWidgets('Init requires clientId', (WidgetTester tester) async { - expect(plugin.init(hostedDomain: ''), throwsAssertionError); + 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', + testWidgets("Init doesn't accept spaces in scopes", (WidgetTester tester) async { expect( - plugin.init( + plugin.initWithParams(const SignInInitParameters( hostedDomain: '', clientId: '', scopes: ['scope with spaces'], - ), + )), throwsAssertionError); }); - group('Successful .init, then', () { + // 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.init( + await plugin.initWithParams(const SignInInitParameters( hostedDomain: 'foo', scopes: ['some', 'scope'], clientId: '1234', - ); + )); await plugin.initialized; }); @@ -170,11 +203,11 @@ void main() { // The pre-configured use case for the instances of the plugin in this test gapiUrl = toBase64Url(gapi_mocks.auth2SignInError()); plugin = GoogleSignInPlugin(); - await plugin.init( + await plugin.initWithParams(const SignInInitParameters( hostedDomain: 'foo', scopes: ['some', 'scope'], clientId: '1234', - ); + )); await plugin.initialized; }); 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 new file mode 100644 index 000000000000..7bfef53f7a23 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart @@ -0,0 +1,51 @@ +// 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 index 5da42283367f..fc753e20d92c 100644 --- 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 @@ -34,9 +34,12 @@ void main() { expect(() { plugin.initialized; }, throwsStateError, - reason: - 'The plugin should throw if checking for `initialized` before calling .init'); - await plugin.init(hostedDomain: '', clientId: ''); + 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, 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 index cc49b2759e7f..84f4e6ee8ba8 100644 --- 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 @@ -12,6 +12,8 @@ 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(() => { 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 index 1447093d4115..b341d1d6b96d 100644 --- 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 @@ -51,10 +51,12 @@ class FakeGoogleUser extends Fake implements gapi.GoogleUser { @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; } 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 index 89f9b55f3ddf..56aa61df136e 100644 --- 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 @@ -6,5 +6,5 @@ import 'dart:convert'; String toBase64Url(String contents) { // Open the file - return 'data:text/javascript;base64,' + base64.encode(utf8.encode(contents)); + return 'data:text/javascript;base64,${base64.encode(utf8.encode(contents))}'; } diff --git a/packages/google_sign_in/google_sign_in_web/example/lib/main.dart b/packages/google_sign_in/google_sign_in_web/example/lib/main.dart index d381fb4af3ab..b23015c811e8 100644 --- a/packages/google_sign_in/google_sign_in_web/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in_web/example/lib/main.dart @@ -5,13 +5,16 @@ import 'package:flutter/material.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } /// App for testing class MyApp extends StatefulWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { 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 5a51cd567275..e5abdacf944d 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.2.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -16,6 +16,7 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter + google_sign_in_platform_interface: ^2.2.0 http: ^0.13.0 integration_test: sdk: flutter 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 731ced5ddbbe..c305cae2a33d 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 @@ -41,13 +41,15 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { late Future _isAuthInitialized; bool _isInitCalled = false; - // This method throws if init 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. + // 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. void _assertIsInitCalled() { if (!_isInitCalled) { throw StateError( - 'GoogleSignInPlugin::init() must be called before any other method in this plugin.'); + 'GoogleSignInPlugin::init() or GoogleSignInPlugin::initWithParams() ' + 'must be called before any other method in this plugin.', + ); } } @@ -71,27 +73,41 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { SignInOption signInOption = SignInOption.standard, String? hostedDomain, String? clientId, - }) async { - final String? appClientId = clientId ?? _autoDetectedClientId; + }) { + return initWithParams(SignInInitParameters( + scopes: scopes, + signInOption: signInOption, + hostedDomain: hostedDomain, + clientId: clientId, + )); + } + + @override + Future initWithParams(SignInInitParameters params) async { + final String? appClientId = params.clientId ?? _autoDetectedClientId; assert( appClientId != null, 'ClientID not set. Either set it on a ' ' tag,' - ' or pass clientId when calling init()'); + ' or pass clientId when initializing GoogleSignIn'); + + assert(params.serverClientId == null, + 'serverClientId is not supported on Web.'); assert( - !scopes.any((String scope) => scope.contains(' ')), - 'OAuth 2.0 Scopes for Google APIs can\'t contain spaces.' + !params.scopes.any((String scope) => scope.contains(' ')), + "OAuth 2.0 Scopes for Google APIs can't contain spaces. " 'Check https://developers.google.com/identity/protocols/googlescopes ' 'for a list of valid OAuth 2.0 scopes.'); await _isGapiInitialized; final auth2.GoogleAuth auth = auth2.init(auth2.ClientConfig( - hosted_domain: hostedDomain, + hosted_domain: params.hostedDomain, // The js lib wants a space-separated list of values - scope: scopes.join(' '), + scope: params.scopes.join(' '), client_id: appClientId!, + plugin_name: 'dart-google_sign_in_web', )); final Completer isAuthInitialized = Completer(); diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart index 88d196bad007..f474e0d00f69 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart @@ -12,7 +12,7 @@ // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi.auth2 -// ignore_for_file: public_member_api_docs, unused_element, non_constant_identifier_names, sort_constructors_first, always_specify_types +// ignore_for_file: public_member_api_docs, unused_element, non_constant_identifier_names, sort_constructors_first, always_specify_types, strict_raw_type @JS() library gapiauth2; @@ -57,8 +57,8 @@ class GoogleAuth { /// Calls the onInit function when the GoogleAuth object is fully initialized, or calls the onFailure function if /// initialization fails. - external dynamic then(dynamic onInit(GoogleAuth googleAuth), - [dynamic onFailure(GoogleAuthInitFailureError reason)]); + external dynamic then(dynamic Function(GoogleAuth googleAuth) onInit, + [dynamic Function(GoogleAuthInitFailureError reason) onFailure]); /// Signs out all accounts from the application. external dynamic signOut(); @@ -70,8 +70,8 @@ class GoogleAuth { external dynamic attachClickHandler( dynamic container, SigninOptions options, - dynamic onsuccess(GoogleUser googleUser), - dynamic onfailure(String reason)); + dynamic Function(GoogleUser googleUser) onsuccess, + dynamic Function(String reason) onfailure); } @anonymous @@ -104,7 +104,7 @@ abstract class IsSignedIn { external bool get(); /// Listen for changes in the current user's sign-in state. - external void listen(dynamic listener(bool signedIn)); + external void listen(dynamic Function(bool signedIn) listener); } @anonymous @@ -116,7 +116,7 @@ abstract class CurrentUser { external GoogleUser get(); /// Listen for changes in currentUser. - external void listen(dynamic listener(GoogleUser user)); + external void listen(dynamic Function(GoogleUser user) listener); } @anonymous @@ -233,15 +233,23 @@ abstract class ClientConfig { /// The default redirect_uri is the current URL stripped of query parameters and hash fragment. external String? get redirect_uri; external set redirect_uri(String? v); - external 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}); + + /// 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); + + 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, + }); } @JS('gapi.auth2.SigninOptionsBuilder') @@ -432,7 +440,7 @@ external GoogleAuth? getAuthInstance(); /// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeparams-callback @JS('gapi.auth2.authorize') external void authorize( - AuthorizeConfig params, void callback(AuthorizeResponse response)); + AuthorizeConfig params, void Function(AuthorizeResponse response) callback); // End module gapi.auth2 // Module gapi.signin2 @@ -489,6 +497,7 @@ external void render( @JS() abstract class Promise { external factory Promise( - void executor(void resolve(T result), Function reject)); - external Promise then(void onFulfilled(T result), [Function onRejected]); + void Function(void Function(T result) resolve, Function reject) executor); + external Promise then(void Function(T result) onFulfilled, + [Function onRejected]); } 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 cae20d28db44..72424d8ea15b 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 @@ -29,6 +29,7 @@ Future injectJSLibraries( 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); 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 3bc05d14e34e..42413e091e6e 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.0+5 +version: 0.10.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: @@ -22,7 +22,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - google_sign_in_platform_interface: ^2.0.0 + google_sign_in_platform_interface: ^2.2.0 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 9ab762a3de13..76192566b18b 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,34 @@ +## 0.8.6 + +* Updates minimum Flutter version to 2.10. +* Fixes avoid_redundant_argument_values lint warnings and minor typos. +* Adds `requestFullMetadata` option to `pickImage`, so images on iOS can be picked without `Photo Library Usage` permission. + +## 0.8.5+3 + +* Adds argument error assertions to the app-facing package, to ensure + consistency across platform implementations. +* Updates tests to use a mock platform instead of relying on default + method channel implementation internals. + +## 0.8.5+2 + +* Minor fixes for new analysis options. + +## 0.8.5+1 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.8.5 + +* Moves Android and iOS implementations to federated packages. +* Adds OS version support information to README. + +## 0.8.4+11 + +* Fixes Activity leak. + ## 0.8.4+10 * iOS: allows picking images with WebP format. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 46a7795b748a..aadfc83ff5e6 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -5,6 +5,10 @@ A Flutter plugin for iOS and Android for picking images from the image library, and taking new pictures with the camera. +| | Android | iOS | Web | +|-------------|---------|--------|----------------------------------| +| **Support** | SDK 21+ | iOS 9+ | [See `image_picker_for_web `][1] | + ## Installation First, add `image_picker` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). @@ -19,6 +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`. * `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. @@ -26,9 +31,9 @@ Add the following keys to your _Info.plist_ file, located in `/ios Starting with version **0.8.1** the Android implementation support to pick (multiple) images on Android 4.3 or higher. -No configuration required - the plugin should work out of the box. It is +No configuration required - the plugin should work out of the box. It is however highly recommended to prepare for Android killing the application when -low on memory. How to prepare for this is discussed in the [Handling +low on memory. How to prepare for this is discussed in the [Handling MainActivity destruction on Android](#handling-mainactivity-destruction-on-android) section. @@ -60,12 +65,12 @@ import 'package:image_picker/image_picker.dart'; ### Handling MainActivity destruction on Android When under high memory pressure the Android system may kill the MainActivity of -the application using the image_picker. On Android the image_picker makes use -of the default `Intent.ACTION_GET_CONTENT` or `MediaStore.ACTION_IMAGE_CAPTURE` -intents. This means that while the intent is executing the source application +the application using the image_picker. On Android the image_picker makes use +of the default `Intent.ACTION_GET_CONTENT` or `MediaStore.ACTION_IMAGE_CAPTURE` +intents. This means that while the intent is executing the source application is moved to the background and becomes eligable for cleanup when the system is -low on memory. When the intent finishes executing, Android will restart the -application. Since the data is never returned to the original call use the +low on memory. When the intent finishes executing, Android will restart the +application. Since the data is never returned to the original call use the `ImagePicker.retrieveLostData()` method to retrieve the lost data. For example: ```dart @@ -85,9 +90,9 @@ Future getLostData() async { } ``` -This check should always be run at startup in order to detect and handle this -case. Please refer to the -[example app](https://pub.dev/packages/image_picker/example) for a more +This check should always be run at startup in order to detect and handle this +case. Please refer to the +[example app](https://pub.dev/packages/image_picker/example) for a more complete example of handling this flow. ## Migrating to 0.8.2+ @@ -101,4 +106,6 @@ Starting with version **0.8.2** of the image_picker plugin, new methods have bee | `PickedFile image = await _picker.getImage(...)` | `XFile image = await _picker.pickImage(...)` | | `List images = await _picker.getMultiImage(...)` | `List images = await _picker.pickMultiImage(...)` | | `PickedFile video = await _picker.getVideo(...)` | `XFile video = await _picker.pickVideo(...)` | -| `LostData response = await _picker.getLostData()` | `LostDataResponse response = await _picker.retrieveLostData()` | \ No newline at end of file +| `LostData response = await _picker.getLostData()` | `LostDataResponse response = await _picker.retrieveLostData()` | + +[1]: https://pub.dev/packages/image_picker_for_web#limitations-on-the-web-platform diff --git a/packages/image_picker/image_picker/android/settings.gradle b/packages/image_picker/image_picker/android/settings.gradle deleted file mode 100755 index 5b9496172108..000000000000 --- a/packages/image_picker/image_picker/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'imagepicker' diff --git a/packages/image_picker/image_picker/example/README.md b/packages/image_picker/image_picker/example/README.md index 129aa856c8f2..18497eb11032 100755 --- a/packages/image_picker/image_picker/example/README.md +++ b/packages/image_picker/image_picker/example/README.md @@ -1,8 +1,3 @@ # image_picker_example Demonstrates how to use the image_picker plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/image_picker/image_picker/example/android/app/build.gradle b/packages/image_picker/image_picker/example/android/app/build.gradle index e73e3fe01003..e83cb5a13c06 100755 --- a/packages/image_picker/image_picker/example/android/app/build.gradle +++ b/packages/image_picker/image_picker/example/android/app/build.gradle @@ -60,7 +60,7 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' api 'androidx.test:core:1.2.0' diff --git a/packages/image_picker/image_picker/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/image_picker/image_picker/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/image_picker/image_picker/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/image_picker/image_picker/example/android/build.gradle b/packages/image_picker/image_picker/example/android/build.gradle index e101ac08df55..c21bff8e0a2f 100755 --- a/packages/image_picker/image_picker/example/android/build.gradle +++ b/packages/image_picker/image_picker/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:7.0.1' } } diff --git a/packages/image_picker/image_picker/example/android/gradle.properties b/packages/image_picker/image_picker/example/android/gradle.properties index 6effed032590..38c8d4544ff1 100755 --- a/packages/image_picker/image_picker/example/android/gradle.properties +++ b/packages/image_picker/image_picker/example/android/gradle.properties @@ -2,4 +2,3 @@ org.gradle.jvmargs=-Xmx1536M android.enableR8=true android.useAndroidX=true android.enableJetifier=true -android.enableUnitTestBinaryResources=true diff --git a/packages/image_picker/image_picker/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker/example/android/gradle/wrapper/gradle-wrapper.properties index 019065d1d650..297f2fec363f 100644 --- a/packages/image_picker/image_picker/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/image_picker/image_picker/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/image_picker/image_picker/example/ios/Podfile b/packages/image_picker/image_picker/example/ios/Podfile index 5bc7b7e85717..f7d6a5e68c3a 100644 --- a/packages/image_picker/image_picker/example/ios/Podfile +++ b/packages/image_picker/image_picker/example/ios/Podfile @@ -29,13 +29,6 @@ flutter_ios_podfile_setup target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - - target 'RunnerTests' do - platform :ios, '9.0' - inherit! :search_paths - # Pods for testing - pod 'OCMock', '~> 3.8.1' - end end post_install do |installer| diff --git a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj index 2847bfd85046..589858f39019 100644 --- a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,53 +3,20 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ - 334733FC266813EE00DCC49E /* ImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */; }; - 334733FD266813F100DCC49E /* MetaDataUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 680049252280D736006DD6AB /* MetaDataUtilTests.m */; }; - 334733FE266813F400DCC49E /* PhotoAssetUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */; }; - 334733FF266813FA00DCC49E /* ImagePickerTestImages.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */; }; - 33473400266813FD00DCC49E /* ImagePickerPluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */; }; - 3A72BAD3FAE6E0FA9D80826B /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 35AE65F25E0B8C8214D8372B /* libPods-RunnerTests.a */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 5C9513011EC38BD300040975 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */; }; - 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 */; }; - 86430DF9272D71E9002D9D6C /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; - 86E9A893272754860017E6E0 /* PickerSaveImageToPathOperationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */; }; - 86E9A894272754A30017E6E0 /* webpImage.webp in Resources */ = {isa = PBXBuildFile; fileRef = 86E9A88F272747B90017E6E0 /* webpImage.webp */; }; - 86E9A895272769130017E6E0 /* pngImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 680049352280F2B8006DD6AB /* pngImage.png */; }; - 86E9A896272769150017E6E0 /* jpgImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 680049362280F2B8006DD6AB /* jpgImage.jpg */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 9FC8F0E9229FA49E00C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; - 9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; - BE6173D826A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */; }; F4F7A436CCA4BF276270A3AE /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EC32F6993F4529982D9519F1 /* libPods-Runner.a */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 334733F72668136400DCC49E /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; - 6801C83B2555D726009DAF8D /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -65,27 +32,18 @@ /* Begin PBXFileReference section */ 0C7B151765FD4249454C49AD /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - 334733F22668136400DCC49E /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 334733F62668136400DCC49E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35AE65F25E0B8C8214D8372B /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 5A9D31B91557877A0E8EF3E7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 5C9512FF1EC38BD300040975 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 680049252280D736006DD6AB /* MetaDataUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MetaDataUtilTests.m; sourceTree = ""; }; 680049352280F2B8006DD6AB /* pngImage.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = pngImage.png; sourceTree = ""; }; 680049362280F2B8006DD6AB /* jpgImage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = jpgImage.jpg; sourceTree = ""; }; 6801632E632668F4349764C9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 6801C8362555D726009DAF8D /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromGalleryUITests.m; sourceTree = ""; }; - 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 = ""; }; 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 = ""; }; 86E9A88F272747B90017E6E0 /* webpImage.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = webpImage.webp; sourceTree = ""; }; - 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PickerSaveImageToPathOperationTests.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -95,30 +53,11 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = gifImage.gif; sourceTree = ""; }; - 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImageUtilTests.m; sourceTree = ""; }; - BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromLimitedGalleryUITests.m; sourceTree = ""; }; DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; EC32F6993F4529982D9519F1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ImagePickerTestImages.h; sourceTree = ""; }; - F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerTestImages.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 334733EF2668136400DCC49E /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 3A72BAD3FAE6E0FA9D80826B /* libPods-RunnerTests.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 6801C8332555D726009DAF8D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -130,21 +69,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 334733F32668136400DCC49E /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */, - 680049252280D736006DD6AB /* MetaDataUtilTests.m */, - 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */, - F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */, - F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */, - 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */, - 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */, - 334733F62668136400DCC49E /* Info.plist */, - ); - path = RunnerTests; - sourceTree = ""; - }; 680049282280E33D006DD6AB /* TestImages */ = { isa = PBXGroup; children = ( @@ -156,16 +80,6 @@ path = TestImages; sourceTree = ""; }; - 6801C8372555D726009DAF8D /* RunnerUITests */ = { - isa = PBXGroup; - children = ( - BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */, - 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */, - 6801C83A2555D726009DAF8D /* Info.plist */, - ); - path = RunnerUITests; - sourceTree = ""; - }; 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { isa = PBXGroup; children = ( @@ -194,8 +108,6 @@ 680049282280E33D006DD6AB /* TestImages */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - 334733F32668136400DCC49E /* RunnerTests */, - 6801C8372555D726009DAF8D /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, 840012C8B5EDBCF56B0E4AC1 /* Pods */, CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, @@ -206,8 +118,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - 6801C8362555D726009DAF8D /* RunnerUITests.xctest */, - 334733F22668136400DCC49E /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -248,43 +158,6 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 334733F12668136400DCC49E /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 334733F92668136400DCC49E /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - B8739A4353234497CF76B597 /* [CP] Check Pods Manifest.lock */, - 334733EE2668136400DCC49E /* Sources */, - 334733EF2668136400DCC49E /* Frameworks */, - 334733F02668136400DCC49E /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 334733F82668136400DCC49E /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 334733F22668136400DCC49E /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 6801C8352555D726009DAF8D /* RunnerUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 6801C83F2555D726009DAF8D /* Build configuration list for PBXNativeTarget "RunnerUITests" */; - buildPhases = ( - 6801C8322555D726009DAF8D /* Sources */, - 6801C8332555D726009DAF8D /* Frameworks */, - 6801C8342555D726009DAF8D /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 6801C83C2555D726009DAF8D /* PBXTargetDependency */, - ); - name = RunnerUITests; - productName = RunnerUITests; - productReference = 6801C8362555D726009DAF8D /* RunnerUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -316,16 +189,6 @@ LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { - 334733F12668136400DCC49E = { - CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 6801C8352555D726009DAF8D = { - CreatedOnToolsVersion = 11.7; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; SystemCapabilities = { @@ -350,34 +213,11 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - 334733F12668136400DCC49E /* RunnerTests */, - 6801C8352555D726009DAF8D /* RunnerUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 334733F02668136400DCC49E /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 86430DF9272D71E9002D9D6C /* gifImage.gif in Resources */, - 86E9A894272754A30017E6E0 /* webpImage.webp in Resources */, - 86E9A895272769130017E6E0 /* pngImage.png in Resources */, - 86E9A896272769150017E6E0 /* jpgImage.jpg in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 6801C8342555D726009DAF8D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */, - 680049382280F2B9006DD6AB /* pngImage.png in Resources */, - 680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -438,53 +278,9 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - B8739A4353234497CF76B597 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 334733EE2668136400DCC49E /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 334733FD266813F100DCC49E /* MetaDataUtilTests.m in Sources */, - 334733FF266813FA00DCC49E /* ImagePickerTestImages.m in Sources */, - 86E9A893272754860017E6E0 /* PickerSaveImageToPathOperationTests.m in Sources */, - 334733FC266813EE00DCC49E /* ImageUtilTests.m in Sources */, - 33473400266813FD00DCC49E /* ImagePickerPluginTests.m in Sources */, - 334733FE266813F400DCC49E /* PhotoAssetUtilTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 6801C8322555D726009DAF8D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 6801C8392555D726009DAF8D /* ImagePickerFromGalleryUITests.m in Sources */, - BE6173D826A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -497,19 +293,6 @@ }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 334733F82668136400DCC49E /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 334733F72668136400DCC49E /* PBXContainerItemProxy */; - }; - 6801C83C2555D726009DAF8D /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 6801C83B2555D726009DAF8D /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -530,79 +313,6 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 334733FA2668136400DCC49E /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = RunnerTests/Info.plist; - 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"; - }; - name = Debug; - }; - 334733FB2668136400DCC49E /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 0C7B151765FD4249454C49AD /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = RunnerTests/Info.plist; - 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"; - }; - name = Release; - }; - 6801C83D2555D726009DAF8D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Runner; - }; - name = Debug; - }; - 6801C83E2555D726009DAF8D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Runner; - }; - name = Release; - }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -754,24 +464,6 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 334733F92668136400DCC49E /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 334733FA2668136400DCC49E /* Debug */, - 334733FB2668136400DCC49E /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 6801C83F2555D726009DAF8D /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 6801C83D2555D726009DAF8D /* Debug */, - 6801C83E2555D726009DAF8D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index f3ad2375b8f2..5e448ddbee68 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -13,10 +13,12 @@ import 'package:image_picker/image_picker.dart'; import 'package:video_player/video_player.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return const MaterialApp( @@ -32,13 +34,13 @@ class MyHomePage extends StatefulWidget { final String? title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { List? _imageFileList; - set _imageFile(XFile? value) { + void _setImageFileListFromFile(XFile? value) { _imageFileList = value == null ? null : [value]; } @@ -91,7 +93,7 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context!, (double? maxWidth, double? maxHeight, int? quality) async { try { - final List? pickedFileList = await _picker.pickMultiImage( + final List pickedFileList = await _picker.pickMultiImage( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality, @@ -116,7 +118,7 @@ class _MyHomePageState extends State { imageQuality: quality, ); setState(() { - _imageFile = pickedFile; + _setImageFileListFromFile(pickedFile); }); } catch (e) { setState(() { @@ -177,21 +179,22 @@ class _MyHomePageState extends State { } if (_imageFileList != null) { return Semantics( - 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)), - ); - }, - itemCount: _imageFileList!.length, - ), - label: 'image_picker_example_picked_images'); + label: 'image_picker_example_picked_images', + child: ListView.builder( + key: UniqueKey(), + itemBuilder: (BuildContext context, int index) { + // Why network for web? + // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform + return Semantics( + label: 'image_picker_example_picked_image', + child: kIsWeb + ? Image.network(_imageFileList![index].path) + : Image.file(File(_imageFileList![index].path)), + ); + }, + itemCount: _imageFileList!.length, + ), + ); } else if (_pickImageError != null) { return Text( 'Pick image error: $_pickImageError', @@ -225,8 +228,11 @@ class _MyHomePageState extends State { } else { isVideo = false; setState(() { - _imageFile = response.file; - _imageFileList = response.files; + if (response.files == null) { + _setImageFileListFromFile(response.file); + } else { + _imageFileList = response.files; + } }); } } else { @@ -417,7 +423,7 @@ typedef OnPickImageCallback = void Function( double? maxWidth, double? maxHeight, int? quality); class AspectRatioVideo extends StatefulWidget { - const AspectRatioVideo(this.controller); + const AspectRatioVideo(this.controller, {Key? key}) : super(key: key); final VideoPlayerController? controller; diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index 28b37197d8ff..e9511e27ab6d 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.5.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -20,9 +20,11 @@ dependencies: video_player: ^2.1.4 dev_dependencies: - espresso: ^0.1.0+2 + espresso: ^0.2.0 flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin_Test.h b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin_Test.h deleted file mode 100644 index 5442f7d089c6..000000000000 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin_Test.h +++ /dev/null @@ -1,54 +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 header is available in the Test module. Import via "@import image_picker.Test;" - -#import - -/** Methods exposed for unit testing. */ -@interface FLTImagePickerPlugin () - -/** The Flutter result callback use to report results back to Flutter App. */ -@property(copy, nonatomic) FlutterResult result; - -/** - * Applies NSMutableArray on the FLutterResult. - * - * NSString must be returned by FlutterResult if the single image - * mode is active. It is checked by maxImagesAllowed and - * returns the first object of the pathlist. - * - * NSMutableArray must be returned by FlutterResult if the multi-image - * mode is active. After the pathlist count is checked then it returns - * the pathlist. - * - * @param pathList that should be applied to FlutterResult. - */ -- (void)handleSavedPathList:(NSArray *)pathList; - -/** - * Tells the delegate that the user cancelled the pick operation. - * - * Your delegate’s implementation of this method should dismiss the picker view - * by calling the dismissModalViewControllerAnimated: method of the parent - * view controller. - * - * Implementation of this method is optional, but expected. - * - * @param picker The controller object managing the image picker interface. - */ -- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker; - -/** - * Sets UIImagePickerController instances that will be used when a new - * controller would normally be created. Each call to - * createImagePickerController will remove the current first element from - * the array. - * - * Should be used for testing purposes only. - */ -- (void)setImagePickerControllerOverrides: - (NSArray *)imagePickerControllers; - -@end diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index 5bc99d7f0bb2..2e266ccd5a5a 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -173,8 +173,9 @@ class ImagePicker { /// The `source` argument controls where the image comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. /// - /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and above only support HEIC images if used - /// in addition to a size modification, of which the usage is explained below. + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and + /// above only support HEIC images if used in addition to a size modification, + /// of which the usage is explained below. /// /// If specified, the image will be at most `maxWidth` wide and /// `maxHeight` tall. Otherwise the image will be returned at it's @@ -182,14 +183,22 @@ class ImagePicker { /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100 /// where 100 is the original/max quality. If `imageQuality` is null, the image with /// the original quality will be returned. Compression is only supported for certain - /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, - /// a warning message will be logged. - /// - /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. - /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. - /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if - /// the front or rear camera should be opened, this function is not guaranteed - /// to work on an Android device. + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not + /// supported for the image that is picked, a warning message will be logged. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is + /// [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. + /// It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter + /// for an intent to specify if the front or rear camera should be opened, this + /// function is not guaranteed to work on an Android device. + /// + /// Use `requestFullMetadata` (defaults to `true`) to control how much additional + /// information the plugin tries to get. + /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full + /// image metadata which may require extra permission requests on some platforms, + /// such as `Photo Library Usage` permission on iOS. /// /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. @@ -206,13 +215,28 @@ class ImagePicker { double? maxHeight, int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, + bool requestFullMetadata = true, }) { - return platform.getImage( + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + + return platform.getImageFromSource( source: source, - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: imageQuality, - preferredCameraDevice: preferredCameraDevice, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + requestFullMetadata: requestFullMetadata, + ), ); } @@ -228,11 +252,18 @@ class ImagePicker { /// If specified, the images will be at most `maxWidth` wide and /// `maxHeight` tall. Otherwise the images will be returned at it's /// original width and height. + /// /// The `imageQuality` argument modifies the quality of the images, ranging from 0-100 /// where 100 is the original/max quality. If `imageQuality` is null, the images with /// the original quality will be returned. Compression is only supported for certain - /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, - /// a warning message will be logged. + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not + /// supported for the image that is picked, a warning message will be logged. + /// + /// Use `requestFullMetadata` (defaults to `true`) to control how much additional + /// information the plugin tries to get. + /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full + /// image metadata which may require extra permission requests on some platforms, + /// such as `Photo Library Usage` permission on iOS. /// /// The method could throw [PlatformException] if the app does not have permission to access /// the camera or photos gallery, no camera is available, plugin is already in use, @@ -240,15 +271,32 @@ class ImagePicker { /// be allocated (Android only) or due to an unknown error. /// /// See also [pickImage] to allow users to only pick a single image. - Future?> pickMultiImage({ + Future> pickMultiImage({ double? maxWidth, double? maxHeight, int? imageQuality, + bool requestFullMetadata = true, }) { - return platform.getMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: imageQuality, + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + + return platform.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + requestFullMetadata: requestFullMetadata, + ), + ), ); } diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 4774711129da..7fed3bf4637b 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,31 +3,33 @@ 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.4+10 +version: 0.8.6 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: platforms: android: - package: io.flutter.plugins.imagepicker - pluginClass: ImagePickerPlugin + default_package: image_picker_android ios: - pluginClass: FLTImagePickerPlugin + default_package: image_picker_ios web: default_package: image_picker_for_web dependencies: flutter: sdk: flutter - flutter_plugin_android_lifecycle: ^2.0.1 + image_picker_android: ^0.8.4+11 image_picker_for_web: ^2.1.0 - image_picker_platform_interface: ^2.3.0 + image_picker_ios: ^0.8.6+1 + image_picker_platform_interface: ^2.6.1 dev_dependencies: + build_runner: ^2.1.10 + cross_file: ^0.3.1+1 # Mockito generates a direct include. flutter_test: sdk: flutter mockito: ^5.0.0 diff --git a/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart b/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart index 00049e14f808..9ca4d9c977e8 100644 --- a/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart @@ -14,63 +14,46 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface. import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$ImagePicker', () { - const MethodChannel channel = - MethodChannel('plugins.flutter.io/image_picker'); +import 'image_picker_test.mocks.dart' as base_mock; - final List log = []; +// Add the mixin to make the platform interface accept the mock. +class MockImagePickerPlatform extends base_mock.MockImagePickerPlatform + with MockPlatformInterfaceMixin {} - final ImagePicker picker = ImagePicker(); +void main() { + group('ImagePicker', () { + late MockImagePickerPlatform mockPlatform; - test('ImagePicker platform instance overrides the actual platform used', - () { - final ImagePickerPlatform savedPlatform = ImagePickerPlatform.instance; - final MockPlatform mockPlatform = MockPlatform(); + setUp(() { + mockPlatform = MockImagePickerPlatform(); ImagePickerPlatform.instance = mockPlatform; - expect(ImagePicker.platform, mockPlatform); - ImagePickerPlatform.instance = savedPlatform; }); group('#Single image/video', () { setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return ''; - }); - - log.clear(); + when(mockPlatform.pickImage( + source: anyNamed('source'), + maxWidth: anyNamed('maxWidth'), + maxHeight: anyNamed('maxHeight'), + imageQuality: anyNamed('imageQuality'), + preferredCameraDevice: anyNamed('preferredCameraDevice'))) + .thenAnswer((Invocation _) async => null); }); group('#pickImage', () { test('passes the image source argument correctly', () async { + final ImagePicker picker = ImagePicker(); await picker.getImage(source: ImageSource.camera); await picker.getImage(source: ImageSource.gallery); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 1, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - ], - ); + verifyInOrder([ + mockPlatform.pickImage(source: ImageSource.camera), + mockPlatform.pickImage(source: ImageSource.gallery), + ]); }); test('passes the width and height arguments correctly', () async { + final ImagePicker picker = ImagePicker(); await picker.getImage(source: ImageSource.camera); await picker.getImage( source: ImageSource.camera, @@ -95,277 +78,164 @@ void main() { maxHeight: 20.0, imageQuality: 70); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - ], - ); - }); - - test('does not accept a negative width or height argument', () { - expect( - picker.getImage(source: ImageSource.camera, maxWidth: -1.0), - throwsArgumentError, - ); - - expect( - picker.getImage(source: ImageSource.camera, maxHeight: -1.0), - throwsArgumentError, - ); + verifyInOrder([ + mockPlatform.pickImage(source: ImageSource.camera), + mockPlatform.pickImage(source: ImageSource.camera, maxWidth: 10.0), + mockPlatform.pickImage(source: ImageSource.camera, maxHeight: 10.0), + mockPlatform.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ), + mockPlatform.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + imageQuality: 70, + ), + mockPlatform.pickImage( + source: ImageSource.camera, + maxHeight: 10.0, + imageQuality: 70, + ), + mockPlatform.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + ]); }); - test('handles a null image path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); + test('handles a null image file response gracefully', () async { + final ImagePicker picker = ImagePicker(); expect(await picker.getImage(source: ImageSource.gallery), isNull); expect(await picker.getImage(source: ImageSource.camera), isNull); }); test('camera position defaults to back', () async { + final ImagePicker picker = ImagePicker(); await picker.getImage(source: ImageSource.camera); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - }), - ], - ); + verify(mockPlatform.pickImage(source: ImageSource.camera)); }); test('camera position can set to front', () async { + final ImagePicker picker = ImagePicker(); await picker.getImage( source: ImageSource.camera, preferredCameraDevice: CameraDevice.front); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 1, - }), - ], - ); + verify(mockPlatform.pickImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front)); }); }); group('#pickVideo', () { + setUp(() { + when(mockPlatform.pickVideo( + source: anyNamed('source'), + preferredCameraDevice: anyNamed('preferredCameraDevice'), + maxDuration: anyNamed('maxDuration'))) + .thenAnswer((Invocation _) async => null); + }); + test('passes the image source argument correctly', () async { + final ImagePicker picker = ImagePicker(); await picker.getVideo(source: ImageSource.camera); await picker.getVideo(source: ImageSource.gallery); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - isMethodCall('pickVideo', arguments: { - 'source': 1, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); + verifyInOrder([ + mockPlatform.pickVideo(source: ImageSource.camera), + mockPlatform.pickVideo(source: ImageSource.gallery), + ]); }); test('passes the duration argument correctly', () async { + final ImagePicker picker = ImagePicker(); await picker.getVideo(source: ImageSource.camera); await picker.getVideo( source: ImageSource.camera, maxDuration: const Duration(seconds: 10)); - await picker.getVideo( - source: ImageSource.camera, - maxDuration: const Duration(minutes: 1)); - await picker.getVideo( + + verifyInOrder([ + mockPlatform.pickVideo(source: ImageSource.camera), + mockPlatform.pickVideo( source: ImageSource.camera, - maxDuration: const Duration(hours: 1)); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 10, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 60, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 3600, - 'cameraDevice': 0, - }), - ], - ); + maxDuration: const Duration(seconds: 10), + ), + ]); }); - test('handles a null video path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); + test('handles a null video file response gracefully', () async { + final ImagePicker picker = ImagePicker(); expect(await picker.getVideo(source: ImageSource.gallery), isNull); expect(await picker.getVideo(source: ImageSource.camera), isNull); }); test('camera position defaults to back', () async { + final ImagePicker picker = ImagePicker(); await picker.getVideo(source: ImageSource.camera); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); + verify(mockPlatform.pickVideo(source: ImageSource.camera)); }); test('camera position can set to front', () async { + final ImagePicker picker = ImagePicker(); await picker.getVideo( source: ImageSource.camera, preferredCameraDevice: CameraDevice.front); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 1, - }), - ], - ); + verify(mockPlatform.pickVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front)); }); }); group('#retrieveLostData', () { test('retrieveLostData get success response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path', - }; - }); + final ImagePicker picker = ImagePicker(); + when(mockPlatform.retrieveLostData()).thenAnswer( + (Invocation _) async => LostData( + file: PickedFile('/example/path'), type: RetrieveType.image)); + final LostData response = await picker.getLostData(); + expect(response.type, RetrieveType.image); expect(response.file!.path, '/example/path'); }); test('retrieveLostData get error response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - }; - }); + final ImagePicker picker = ImagePicker(); + when(mockPlatform.retrieveLostData()).thenAnswer( + (Invocation _) async => LostData( + exception: PlatformException( + code: 'test_error_code', message: 'test_error_message'), + type: RetrieveType.video)); + final LostData response = await picker.getLostData(); + expect(response.type, RetrieveType.video); expect(response.exception!.code, 'test_error_code'); expect(response.exception!.message, 'test_error_message'); }); - - test('retrieveLostData get null response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return null; - }); - expect((await picker.getLostData()).isEmpty, true); - }); - - test('retrieveLostData get both path and error should throw', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - 'path': '/example/path', - }; - }); - expect(picker.getLostData(), throwsAssertionError); - }); }); }); group('Multi images', () { setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return []; - }); - log.clear(); + when(mockPlatform.pickMultiImage( + maxWidth: anyNamed('maxWidth'), + maxHeight: anyNamed('maxHeight'), + imageQuality: anyNamed('imageQuality'))) + .thenAnswer((Invocation _) async => null); }); group('#pickMultiImage', () { test('passes the width and height arguments correctly', () async { + final ImagePicker picker = ImagePicker(); await picker.getMultiImage(); await picker.getMultiImage( maxWidth: 10.0, @@ -388,62 +258,23 @@ void main() { await picker.getMultiImage( maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); - expect( - log, - [ - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - }), - ], - ); - }); - - test('does not accept a negative width or height argument', () { - expect( - picker.getMultiImage(maxWidth: -1.0), - throwsArgumentError, - ); - - expect( - picker.getMultiImage(maxHeight: -1.0), - throwsArgumentError, - ); + verifyInOrder([ + mockPlatform.pickMultiImage(), + mockPlatform.pickMultiImage(maxWidth: 10.0), + mockPlatform.pickMultiImage(maxHeight: 10.0), + mockPlatform.pickMultiImage(maxWidth: 10.0, maxHeight: 20.0), + mockPlatform.pickMultiImage(maxWidth: 10.0, imageQuality: 70), + mockPlatform.pickMultiImage(maxHeight: 10.0, imageQuality: 70), + mockPlatform.pickMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + ]); }); - test('handles a null image path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); + test('handles a null image file response gracefully', () async { + final ImagePicker picker = ImagePicker(); expect(await picker.getMultiImage(), isNull); expect(await picker.getMultiImage(), isNull); @@ -452,7 +283,3 @@ void main() { }); }); } - -class MockPlatform extends Mock - with MockPlatformInterfaceMixin - implements ImagePickerPlatform {} diff --git a/packages/image_picker/image_picker/test/image_picker_test.dart b/packages/image_picker/image_picker/test/image_picker_test.dart index b41fbe3381df..2d959bd20f7b 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -6,66 +6,59 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker/image_picker.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$ImagePicker', () { - const MethodChannel channel = - MethodChannel('plugins.flutter.io/image_picker'); +import 'image_picker_test.mocks.dart' as base_mock; - final List log = []; +// Add the mixin to make the platform interface accept the mock. +class MockImagePickerPlatform extends base_mock.MockImagePickerPlatform + with MockPlatformInterfaceMixin {} - final ImagePicker picker = ImagePicker(); +@GenerateMocks([ImagePickerPlatform]) +void main() { + group('ImagePicker', () { + late MockImagePickerPlatform mockPlatform; - test('ImagePicker platform instance overrides the actual platform used', - () { - final ImagePickerPlatform savedPlatform = ImagePickerPlatform.instance; - final MockPlatform mockPlatform = MockPlatform(); + setUp(() { + mockPlatform = MockImagePickerPlatform(); ImagePickerPlatform.instance = mockPlatform; - expect(ImagePicker.platform, mockPlatform); - ImagePickerPlatform.instance = savedPlatform; }); group('#Single image/video', () { - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return ''; + group('#pickImage', () { + setUp(() { + when(mockPlatform.getImageFromSource( + source: anyNamed('source'), options: anyNamed('options'))) + .thenAnswer((Invocation _) async => null); }); - log.clear(); - }); - - group('#pickImage', () { test('passes the image source argument correctly', () async { + final ImagePicker picker = ImagePicker(); await picker.pickImage(source: ImageSource.camera); await picker.pickImage(source: ImageSource.gallery); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 1, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - ], - ); + verifyInOrder([ + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf(), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.gallery, + options: argThat( + isInstanceOf(), + named: 'options', + ), + ), + ]); }); test('passes the width and height arguments correctly', () async { + final ImagePicker picker = ImagePicker(); await picker.pickImage(source: ImageSource.camera); await picker.pickImage( source: ImageSource.camera, @@ -90,242 +83,297 @@ void main() { maxHeight: 20.0, imageQuality: 70); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - ], - ); + verifyInOrder([ + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', isNull) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', isNull) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + isNull), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', equals(10.0)) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', isNull) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + isNull), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', isNull) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', equals(10.0)) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + isNull), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', equals(10.0)) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', equals(20.0)) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + isNull), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', equals(10.0)) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', isNull) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', isNull) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', equals(10.0)) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', equals(10.0)) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', equals(20.0)) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + ]); }); test('does not accept a negative width or height argument', () { + final ImagePicker picker = ImagePicker(); expect( - picker.pickImage(source: ImageSource.camera, maxWidth: -1.0), + () => picker.pickImage(source: ImageSource.camera, maxWidth: -1.0), throwsArgumentError, ); expect( - picker.pickImage(source: ImageSource.camera, maxHeight: -1.0), + () => picker.pickImage(source: ImageSource.camera, maxHeight: -1.0), throwsArgumentError, ); }); - test('handles a null image path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); + test('handles a null image file response gracefully', () async { + final ImagePicker picker = ImagePicker(); expect(await picker.pickImage(source: ImageSource.gallery), isNull); expect(await picker.pickImage(source: ImageSource.camera), isNull); }); test('camera position defaults to back', () async { + final ImagePicker picker = ImagePicker(); await picker.pickImage(source: ImageSource.camera); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - }), - ], - ); + verify(mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf().having( + (ImagePickerOptions options) => options.preferredCameraDevice, + 'preferredCameraDevice', + equals(CameraDevice.rear)), + named: 'options', + ), + )); }); test('camera position can set to front', () async { + final ImagePicker picker = ImagePicker(); await picker.pickImage( source: ImageSource.camera, preferredCameraDevice: CameraDevice.front); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 1, - }), - ], + verify(mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf().having( + (ImagePickerOptions options) => options.preferredCameraDevice, + 'preferredCameraDevice', + equals(CameraDevice.front)), + named: 'options', + ), + )); + }); + + test('full metadata argument defaults to true', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickImage(source: ImageSource.gallery); + + verify(mockPlatform.getImageFromSource( + source: ImageSource.gallery, + options: argThat( + isInstanceOf().having( + (ImagePickerOptions options) => options.requestFullMetadata, + 'requestFullMetadata', + isTrue), + named: 'options', + ), + )); + }); + + test('passes the full metadata argument correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickImage( + source: ImageSource.gallery, + requestFullMetadata: false, ); + + verify(mockPlatform.getImageFromSource( + source: ImageSource.gallery, + options: argThat( + isInstanceOf().having( + (ImagePickerOptions options) => options.requestFullMetadata, + 'requestFullMetadata', + isFalse), + named: 'options', + ), + )); }); }); group('#pickVideo', () { + setUp(() { + when(mockPlatform.getVideo( + source: anyNamed('source'), + preferredCameraDevice: anyNamed('preferredCameraDevice'), + maxDuration: anyNamed('maxDuration'))) + .thenAnswer((Invocation _) async => null); + }); + test('passes the image source argument correctly', () async { + final ImagePicker picker = ImagePicker(); await picker.pickVideo(source: ImageSource.camera); await picker.pickVideo(source: ImageSource.gallery); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - isMethodCall('pickVideo', arguments: { - 'source': 1, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); + verifyInOrder([ + mockPlatform.getVideo(source: ImageSource.camera), + mockPlatform.getVideo(source: ImageSource.gallery), + ]); }); test('passes the duration argument correctly', () async { + final ImagePicker picker = ImagePicker(); await picker.pickVideo(source: ImageSource.camera); await picker.pickVideo( source: ImageSource.camera, maxDuration: const Duration(seconds: 10)); - await picker.pickVideo( - source: ImageSource.camera, - maxDuration: const Duration(minutes: 1)); - await picker.pickVideo( - source: ImageSource.camera, - maxDuration: const Duration(hours: 1)); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 10, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 60, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 3600, - 'cameraDevice': 0, - }), - ], - ); + + verifyInOrder([ + mockPlatform.getVideo(source: ImageSource.camera), + mockPlatform.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10)), + ]); }); - test('handles a null video path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); + test('handles a null video file response gracefully', () async { + final ImagePicker picker = ImagePicker(); expect(await picker.pickVideo(source: ImageSource.gallery), isNull); expect(await picker.pickVideo(source: ImageSource.camera), isNull); }); test('camera position defaults to back', () async { + final ImagePicker picker = ImagePicker(); await picker.pickVideo(source: ImageSource.camera); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); + verify(mockPlatform.getVideo(source: ImageSource.camera)); }); test('camera position can set to front', () async { + final ImagePicker picker = ImagePicker(); await picker.pickVideo( source: ImageSource.camera, preferredCameraDevice: CameraDevice.front); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 1, - }), - ], - ); + verify(mockPlatform.getVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front)); }); }); group('#retrieveLostData', () { test('retrieveLostData get success response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path', - }; - }); + final ImagePicker picker = ImagePicker(); + final XFile lostFile = XFile('/example/path'); + when(mockPlatform.getLostData()).thenAnswer((Invocation _) async => + LostDataResponse( + file: lostFile, + files: [lostFile], + type: RetrieveType.image)); + final LostDataResponse response = await picker.retrieveLostData(); + expect(response.type, RetrieveType.image); expect(response.file!.path, '/example/path'); }); test('retrieveLostData should successfully retrieve multiple files', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path1', - 'pathList': ['/example/path0', '/example/path1'], - }; - }); + final ImagePicker picker = ImagePicker(); + final List lostFiles = [ + XFile('/example/path0'), + XFile('/example/path1'), + ]; + when(mockPlatform.getLostData()).thenAnswer((Invocation _) async => + LostDataResponse( + file: lostFiles.last, + files: lostFiles, + type: RetrieveType.image)); final LostDataResponse response = await picker.retrieveLostData(); + expect(response.type, RetrieveType.image); expect(response.file, isNotNull); expect(response.file!.path, '/example/path1'); @@ -334,51 +382,34 @@ void main() { }); test('retrieveLostData get error response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - }; - }); + final ImagePicker picker = ImagePicker(); + when(mockPlatform.getLostData()).thenAnswer((Invocation _) async => + LostDataResponse( + exception: PlatformException( + code: 'test_error_code', message: 'test_error_message'), + type: RetrieveType.video)); + final LostDataResponse response = await picker.retrieveLostData(); + expect(response.type, RetrieveType.video); expect(response.exception!.code, 'test_error_code'); expect(response.exception!.message, 'test_error_message'); }); - - test('retrieveLostData get null response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return null; - }); - expect((await picker.retrieveLostData()).isEmpty, true); - }); - - test('retrieveLostData get both path and error should throw', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - 'path': '/example/path', - }; - }); - expect(picker.retrieveLostData(), throwsAssertionError); - }); }); }); group('#Multi images', () { setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return []; - }); - log.clear(); + when( + mockPlatform.getMultiImageWithOptions( + options: anyNamed('options'), + ), + ).thenAnswer((Invocation _) async => []); }); group('#pickMultiImage', () { test('passes the width and height arguments correctly', () async { + final ImagePicker picker = ImagePicker(); await picker.pickMultiImage(); await picker.pickMultiImage( maxWidth: 10.0, @@ -401,71 +432,159 @@ void main() { await picker.pickMultiImage( maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); - expect( - log, - [ - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - }), - ], - ); + verifyInOrder([ + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf(), + named: 'options', + ), + ), + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf().having( + (MultiImagePickerOptions options) => + options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf().having( + (MultiImagePickerOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf() + .having( + (MultiImagePickerOptions options) => + options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MultiImagePickerOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(20.0)), + named: 'options', + ), + ), + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf() + .having( + (MultiImagePickerOptions options) => + options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MultiImagePickerOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf() + .having( + (MultiImagePickerOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)) + .having( + (MultiImagePickerOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf() + .having( + (MultiImagePickerOptions options) => + options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MultiImagePickerOptions options) => + options.imageOptions.maxWidth, + 'maxHeight', + equals(10.0)) + .having( + (MultiImagePickerOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + ]); }); test('does not accept a negative width or height argument', () { + final ImagePicker picker = ImagePicker(); expect( - picker.pickMultiImage(maxWidth: -1.0), + () => picker.pickMultiImage(maxWidth: -1.0), throwsArgumentError, ); expect( - picker.pickMultiImage(maxHeight: -1.0), + () => picker.pickMultiImage(maxHeight: -1.0), throwsArgumentError, ); }); - test('handles a null image path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); + test('handles an empty image file response gracefully', () async { + final ImagePicker picker = ImagePicker(); - expect(await picker.pickMultiImage(), isNull); - expect(await picker.pickMultiImage(), isNull); + expect(await picker.pickMultiImage(), isEmpty); + expect(await picker.pickMultiImage(), isEmpty); + }); + + test('full metadata argument defaults to true', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMultiImage(); + + verify(mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf().having( + (MultiImagePickerOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isTrue), + named: 'options', + ), + )); + }); + + test('passes the full metadata argument correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMultiImage( + requestFullMetadata: false, + ); + + verify(mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf().having( + (MultiImagePickerOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isFalse), + named: 'options', + ), + )); }); }); }); }); } - -class MockPlatform extends Mock - with MockPlatformInterfaceMixin - implements ImagePickerPlatform {} diff --git a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart new file mode 100644 index 000000000000..f749b538665b --- /dev/null +++ b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart @@ -0,0 +1,154 @@ +// Mocks generated by Mockito 5.1.0 from annotations +// in image_picker/test/image_picker_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i4; + +import 'package:cross_file/cross_file.dart' as _i5; +import 'package:image_picker_platform_interface/src/platform_interface/image_picker_platform.dart' + as _i3; +import 'package:image_picker_platform_interface/src/types/types.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +class _FakeLostData_0 extends _i1.Fake implements _i2.LostData {} + +class _FakeLostDataResponse_1 extends _i1.Fake + implements _i2.LostDataResponse {} + +/// A class which mocks [ImagePickerPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockImagePickerPlatform extends _i1.Mock + implements _i3.ImagePickerPlatform { + MockImagePickerPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future<_i2.PickedFile?> pickImage( + {_i2.ImageSource? source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + _i2.CameraDevice? preferredCameraDevice = _i2.CameraDevice.rear}) => + (super.noSuchMethod( + Invocation.method(#pickImage, [], { + #source: source, + #maxWidth: maxWidth, + #maxHeight: maxHeight, + #imageQuality: imageQuality, + #preferredCameraDevice: preferredCameraDevice + }), + returnValue: Future<_i2.PickedFile?>.value()) + as _i4.Future<_i2.PickedFile?>); + + @override + _i4.Future?> pickMultiImage( + {double? maxWidth, double? maxHeight, int? imageQuality}) => + (super.noSuchMethod( + Invocation.method(#pickMultiImage, [], { + #maxWidth: maxWidth, + #maxHeight: maxHeight, + #imageQuality: imageQuality + }), + returnValue: Future?>.value()) + as _i4.Future?>); + + @override + _i4.Future<_i2.PickedFile?> pickVideo( + {_i2.ImageSource? source, + _i2.CameraDevice? preferredCameraDevice = _i2.CameraDevice.rear, + Duration? maxDuration}) => + (super.noSuchMethod( + Invocation.method(#pickVideo, [], { + #source: source, + #preferredCameraDevice: preferredCameraDevice, + #maxDuration: maxDuration + }), + returnValue: Future<_i2.PickedFile?>.value()) + as _i4.Future<_i2.PickedFile?>); + + @override + _i4.Future<_i2.LostData> retrieveLostData() => + (super.noSuchMethod(Invocation.method(#retrieveLostData, []), + returnValue: Future<_i2.LostData>.value(_FakeLostData_0())) + as _i4.Future<_i2.LostData>); + + @override + _i4.Future<_i5.XFile?> getImage( + {_i2.ImageSource? source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + _i2.CameraDevice? preferredCameraDevice = _i2.CameraDevice.rear}) => + (super.noSuchMethod( + Invocation.method(#getImage, [], { + #source: source, + #maxWidth: maxWidth, + #maxHeight: maxHeight, + #imageQuality: imageQuality, + #preferredCameraDevice: preferredCameraDevice + }), + returnValue: Future<_i5.XFile?>.value()) as _i4.Future<_i5.XFile?>); + + @override + _i4.Future?> getMultiImage( + {double? maxWidth, double? maxHeight, int? imageQuality}) => + (super.noSuchMethod( + Invocation.method(#getMultiImage, [], { + #maxWidth: maxWidth, + #maxHeight: maxHeight, + #imageQuality: imageQuality + }), + returnValue: Future?>.value()) + as _i4.Future?>); + + @override + _i4.Future<_i5.XFile?> getVideo( + {_i2.ImageSource? source, + _i2.CameraDevice? preferredCameraDevice = _i2.CameraDevice.rear, + Duration? maxDuration}) => + (super.noSuchMethod( + Invocation.method(#getVideo, [], { + #source: source, + #preferredCameraDevice: preferredCameraDevice, + #maxDuration: maxDuration + }), + returnValue: Future<_i5.XFile?>.value()) as _i4.Future<_i5.XFile?>); + + @override + _i4.Future<_i2.LostDataResponse> getLostData() => + (super.noSuchMethod(Invocation.method(#getLostData, []), + returnValue: + Future<_i2.LostDataResponse>.value(_FakeLostDataResponse_1())) + as _i4.Future<_i2.LostDataResponse>); + + @override + _i4.Future<_i5.XFile?> getImageFromSource( + {_i2.ImageSource? source, + _i2.ImagePickerOptions? options = const _i2.ImagePickerOptions()}) => + (super.noSuchMethod( + Invocation.method( + #getImageFromSource, [], {#source: source, #options: options}), + returnValue: Future<_i5.XFile?>.value()) as _i4.Future<_i5.XFile?>); + + @override + _i4.Future> getMultiImageWithOptions( + {_i2.MultiImagePickerOptions? options = + const _i2.MultiImagePickerOptions()}) => + (super.noSuchMethod( + Invocation.method(#getMultiImageWithOptions, [], {#options: options}), + returnValue: + Future>.value(<_i5.XFile>[])) as _i4 + .Future>); +} diff --git a/packages/image_picker/image_picker_android/AUTHORS b/packages/image_picker/image_picker_android/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/image_picker/image_picker_android/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md new file mode 100644 index 000000000000..3075f5b1d976 --- /dev/null +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -0,0 +1,30 @@ +## 0.8.5+3 + +* Updates minimum Flutter version to 2.10. +* Bumps gradle from 7.1.2 to 7.2.1. + +## 0.8.5+2 + +* Updates `image_picker_platform_interface` constraint to the correct minimum + version. + +## 0.8.5+1 + +* Switches to an internal method channel implementation. + +## 0.8.5 + +* Updates gradle to 7.1.2. + +## 0.8.4+13 + +* Minor fixes for new analysis options. + +## 0.8.4+12 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.8.4+11 + +* Splits from `image_picker` as a federated implementation. diff --git a/packages/image_picker/image_picker_android/LICENSE b/packages/image_picker/image_picker_android/LICENSE new file mode 100644 index 000000000000..0be8bbc3e68d --- /dev/null +++ b/packages/image_picker/image_picker_android/LICENSE @@ -0,0 +1,231 @@ +image_picker + +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. +-------------------------------------------------------------------------------- +aFileChooser + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2011 - 2013 Paul Burke + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/image_picker/image_picker_android/README.md b/packages/image_picker/image_picker_android/README.md new file mode 100755 index 000000000000..43d08c2a8b3a --- /dev/null +++ b/packages/image_picker/image_picker_android/README.md @@ -0,0 +1,11 @@ +# image\_picker\_android + +The Android implementation of [`image_picker`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `image_picker` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/image_picker +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/image_picker/image_picker/android/build.gradle b/packages/image_picker/image_picker_android/android/build.gradle old mode 100755 new mode 100644 similarity index 72% rename from packages/image_picker/image_picker/android/build.gradle rename to packages/image_picker/image_picker_android/android/build.gradle index 928c7cda8b03..aed1ad5174ea --- a/packages/image_picker/image_picker/android/build.gradle +++ b/packages/image_picker/image_picker_android/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:7.2.1' } } @@ -29,18 +29,19 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { + disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' } dependencies { - implementation 'androidx.core:core:1.0.2' - implementation 'androidx.annotation:annotation:1.0.0' - implementation 'androidx.exifinterface:exifinterface:1.3.0' - - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:3.10.0' - testImplementation 'androidx.test:core:1.2.0' - testImplementation "org.robolectric:robolectric:4.3.1" + implementation 'androidx.core:core:1.8.0' + implementation 'androidx.annotation:annotation:1.3.0' + implementation 'androidx.exifinterface:exifinterface:1.3.3' + + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:4.8.0' + testImplementation 'androidx.test:core:1.4.0' + testImplementation "org.robolectric:robolectric:4.8.1" } compileOptions { diff --git a/packages/image_picker/image_picker_android/android/settings.gradle b/packages/image_picker/image_picker_android/android/settings.gradle new file mode 100755 index 000000000000..3c673efcd542 --- /dev/null +++ b/packages/image_picker/image_picker_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'image_picker_android' diff --git a/packages/image_picker/image_picker/android/src/main/AndroidManifest.xml b/packages/image_picker/image_picker_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/image_picker/image_picker/android/src/main/AndroidManifest.xml rename to packages/image_picker/image_picker_android/android/src/main/AndroidManifest.xml diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java diff --git a/packages/image_picker/image_picker/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 similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java similarity index 72% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java index 577675bd433a..8336a145e93a 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java @@ -85,31 +85,111 @@ public void onActivityDestroyed(Activity activity) { @Override public void onActivityStopped(Activity activity) { if (thisActivity == activity) { - delegate.saveStateBeforeResult(); + activityState.getDelegate().saveStateBeforeResult(); } } } + /** + * Move all activity-lifetime-bound states into this helper object, so that {@code setup} and + * {@code tearDown} would just become constructor and finalize calls of the helper object. + */ + private class ActivityState { + private Application application; + private Activity activity; + private ImagePickerDelegate delegate; + private MethodChannel channel; + private LifeCycleObserver observer; + private ActivityPluginBinding activityBinding; + + // This is null when not using v2 embedding; + private Lifecycle lifecycle; + + // Default constructor + ActivityState( + final Application application, + final Activity activity, + final BinaryMessenger messenger, + final MethodChannel.MethodCallHandler handler, + final PluginRegistry.Registrar registrar, + final ActivityPluginBinding activityBinding) { + this.application = application; + this.activity = activity; + this.activityBinding = activityBinding; + + delegate = constructDelegate(activity); + channel = new MethodChannel(messenger, CHANNEL); + channel.setMethodCallHandler(handler); + observer = new LifeCycleObserver(activity); + if (registrar != null) { + // V1 embedding setup for activity listeners. + application.registerActivityLifecycleCallbacks(observer); + registrar.addActivityResultListener(delegate); + registrar.addRequestPermissionsResultListener(delegate); + } else { + // V2 embedding setup for activity listeners. + activityBinding.addActivityResultListener(delegate); + activityBinding.addRequestPermissionsResultListener(delegate); + lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding); + lifecycle.addObserver(observer); + } + } + + // Only invoked by {@link #ImagePickerPlugin(ImagePickerDelegate, Activity)} for testing. + ActivityState(final ImagePickerDelegate delegate, final Activity activity) { + this.activity = activity; + this.delegate = delegate; + } + + void release() { + if (activityBinding != null) { + activityBinding.removeActivityResultListener(delegate); + activityBinding.removeRequestPermissionsResultListener(delegate); + activityBinding = null; + } + + if (lifecycle != null) { + lifecycle.removeObserver(observer); + lifecycle = null; + } + + if (channel != null) { + channel.setMethodCallHandler(null); + channel = null; + } + + if (application != null) { + application.unregisterActivityLifecycleCallbacks(observer); + application = null; + } + + activity = null; + observer = null; + delegate = null; + } + + Activity getActivity() { + return activity; + } + + ImagePickerDelegate getDelegate() { + return delegate; + } + } + static final String METHOD_CALL_IMAGE = "pickImage"; static final String METHOD_CALL_MULTI_IMAGE = "pickMultiImage"; static final String METHOD_CALL_VIDEO = "pickVideo"; private static final String METHOD_CALL_RETRIEVE = "retrieve"; private static final int CAMERA_DEVICE_FRONT = 1; private static final int CAMERA_DEVICE_REAR = 0; - private static final String CHANNEL = "plugins.flutter.io/image_picker"; + private static final String CHANNEL = "plugins.flutter.io/image_picker_android"; private static final int SOURCE_CAMERA = 0; private static final int SOURCE_GALLERY = 1; - private MethodChannel channel; - private ImagePickerDelegate delegate; private FlutterPluginBinding pluginBinding; - private ActivityPluginBinding activityBinding; - private Application application; - private Activity activity; - // This is null when not using v2 embedding; - private Lifecycle lifecycle; - private LifeCycleObserver observer; + private ActivityState activityState; @SuppressWarnings("deprecation") public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { @@ -137,8 +217,12 @@ public ImagePickerPlugin() {} @VisibleForTesting ImagePickerPlugin(final ImagePickerDelegate delegate, final Activity activity) { - this.delegate = delegate; - this.activity = activity; + activityState = new ActivityState(delegate, activity); + } + + @VisibleForTesting + final ActivityState getActivityState() { + return activityState; } @Override @@ -153,13 +237,12 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { @Override public void onAttachedToActivity(ActivityPluginBinding binding) { - activityBinding = binding; setup( pluginBinding.getBinaryMessenger(), (Application) pluginBinding.getApplicationContext(), - activityBinding.getActivity(), + binding.getActivity(), null, - activityBinding); + binding); } @Override @@ -183,37 +266,15 @@ private void setup( final Activity activity, final PluginRegistry.Registrar registrar, final ActivityPluginBinding activityBinding) { - this.activity = activity; - this.application = application; - this.delegate = constructDelegate(activity); - channel = new MethodChannel(messenger, CHANNEL); - channel.setMethodCallHandler(this); - observer = new LifeCycleObserver(activity); - if (registrar != null) { - // V1 embedding setup for activity listeners. - application.registerActivityLifecycleCallbacks(observer); - registrar.addActivityResultListener(delegate); - registrar.addRequestPermissionsResultListener(delegate); - } else { - // V2 embedding setup for activity listeners. - activityBinding.addActivityResultListener(delegate); - activityBinding.addRequestPermissionsResultListener(delegate); - lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding); - lifecycle.addObserver(observer); - } + activityState = + new ActivityState(application, activity, messenger, this, registrar, activityBinding); } private void tearDown() { - activityBinding.removeActivityResultListener(delegate); - activityBinding.removeRequestPermissionsResultListener(delegate); - activityBinding = null; - lifecycle.removeObserver(observer); - lifecycle = null; - delegate = null; - channel.setMethodCallHandler(null); - channel = null; - application.unregisterActivityLifecycleCallbacks(observer); - application = null; + if (activityState != null) { + activityState.release(); + activityState = null; + } } @VisibleForTesting @@ -273,12 +334,13 @@ public void run() { @Override public void onMethodCall(MethodCall call, MethodChannel.Result rawResult) { - if (activity == null) { + if (activityState == null || activityState.getActivity() == null) { rawResult.error("no_activity", "image_picker plugin requires a foreground activity.", null); return; } MethodChannel.Result result = new MethodResultWrapper(rawResult); int imageSource; + ImagePickerDelegate delegate = activityState.getDelegate(); if (call.argument("cameraDevice") != null) { CameraDevice device; int deviceIntValue = call.argument("cameraDevice"); diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java similarity index 100% rename from packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java rename to packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java diff --git a/packages/image_picker/image_picker/android/src/main/res/xml/flutter_image_picker_file_paths.xml b/packages/image_picker/image_picker_android/android/src/main/res/xml/flutter_image_picker_file_paths.xml similarity index 100% rename from packages/image_picker/image_picker/android/src/main/res/xml/flutter_image_picker_file_paths.xml rename to packages/image_picker/image_picker_android/android/src/main/res/xml/flutter_image_picker_file_paths.xml diff --git a/packages/image_picker/image_picker/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 similarity index 100% rename from packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java rename to packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java diff --git a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java similarity index 100% rename from packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java rename to packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java diff --git a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java similarity index 100% rename from packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java rename to packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java diff --git a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java similarity index 78% rename from packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java rename to packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java index 422b8be74f7c..36452479776e 100644 --- a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java @@ -5,17 +5,25 @@ package io.flutter.plugins.imagepicker; import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; 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.verifyZeroInteractions; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import android.app.Activity; import android.app.Application; +import androidx.lifecycle.Lifecycle; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import java.io.File; @@ -41,6 +49,9 @@ public class ImagePickerPluginTest { @Mock io.flutter.plugin.common.PluginRegistry.Registrar mockRegistrar; + @Mock ActivityPluginBinding mockActivityBinding; + @Mock FlutterPluginBinding mockPluginBinding; + @Mock Activity mockActivity; @Mock Application mockApplication; @Mock ImagePickerDelegate mockImagePickerDelegate; @@ -52,7 +63,8 @@ public class ImagePickerPluginTest { public void setUp() { MockitoAnnotations.initMocks(this); when(mockRegistrar.context()).thenReturn(mockApplication); - + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + when(mockPluginBinding.getApplicationContext()).thenReturn(mockApplication); plugin = new ImagePickerPlugin(mockImagePickerDelegate, mockActivity); } @@ -64,7 +76,7 @@ public void onMethodCall_WhenActivityIsNull_FinishesWithForegroundActivityRequir imagePickerPluginWithNullActivity.onMethodCall(call, mockResult); verify(mockResult) .error("no_activity", "image_picker plugin requires a foreground activity.", null); - verifyZeroInteractions(mockImagePickerDelegate); + verifyNoInteractions(mockImagePickerDelegate); } @Test @@ -72,8 +84,8 @@ public void onMethodCall_WhenCalledWithUnknownMethod_ThrowsException() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Unknown method test"); plugin.onMethodCall(new MethodCall("test", null), mockResult); - verifyZeroInteractions(mockImagePickerDelegate); - verifyZeroInteractions(mockResult); + verifyNoInteractions(mockImagePickerDelegate); + verifyNoInteractions(mockResult); } @Test @@ -81,8 +93,8 @@ public void onMethodCall_WhenCalledWithUnknownImageSource_ThrowsException() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Invalid image source: -1"); plugin.onMethodCall(buildMethodCall(PICK_IMAGE, -1), mockResult); - verifyZeroInteractions(mockImagePickerDelegate); - verifyZeroInteractions(mockResult); + verifyNoInteractions(mockImagePickerDelegate); + verifyNoInteractions(mockResult); } @Test @@ -90,7 +102,7 @@ public void onMethodCall_WhenSourceIsGallery_InvokesChooseImageFromGallery() { MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_GALLERY); plugin.onMethodCall(call, mockResult); verify(mockImagePickerDelegate).chooseImageFromGallery(eq(call), any()); - verifyZeroInteractions(mockResult); + verifyNoInteractions(mockResult); } @Test @@ -98,7 +110,7 @@ public void onMethodCall_InvokesChooseMultiImageFromGallery() { MethodCall call = buildMethodCall(PICK_MULTI_IMAGE); plugin.onMethodCall(call, mockResult); verify(mockImagePickerDelegate).chooseMultiImageFromGallery(eq(call), any()); - verifyZeroInteractions(mockResult); + verifyNoInteractions(mockResult); } @Test @@ -106,7 +118,7 @@ public void onMethodCall_WhenSourceIsCamera_InvokesTakeImageWithCamera() { MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA); plugin.onMethodCall(call, mockResult); verify(mockImagePickerDelegate).takeImageWithCamera(eq(call), any()); - verifyZeroInteractions(mockResult); + verifyNoInteractions(mockResult); } @Test @@ -176,6 +188,25 @@ public void constructDelegate_ShouldUseInternalCacheDirectory() { equalTo(mockDirectory)); } + @Test + public void onDetachedFromActivity_ShouldReleaseActivityState() { + final BinaryMessenger mockBinaryMessenger = mock(BinaryMessenger.class); + when(mockPluginBinding.getBinaryMessenger()).thenReturn(mockBinaryMessenger); + + final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class); + when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference); + + final Lifecycle mockLifecycle = mock(Lifecycle.class); + when(mockLifecycleReference.getLifecycle()).thenReturn(mockLifecycle); + + plugin.onAttachedToEngine(mockPluginBinding); + plugin.onAttachedToActivity(mockActivityBinding); + assertNotNull(plugin.getActivityState()); + + plugin.onDetachedFromActivity(); + assertNull(plugin.getActivityState()); + } + private MethodCall buildMethodCall(String method, final int source) { final Map arguments = new HashMap<>(); arguments.put("source", source); diff --git a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java similarity index 100% rename from packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java rename to packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java diff --git a/packages/image_picker/image_picker/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/packages/image_picker/image_picker_android/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker similarity index 100% rename from packages/image_picker/image_picker/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to packages/image_picker/image_picker_android/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/packages/image_picker/image_picker/android/src/test/resources/pngImage.png b/packages/image_picker/image_picker_android/android/src/test/resources/pngImage.png similarity index 100% rename from packages/image_picker/image_picker/android/src/test/resources/pngImage.png rename to packages/image_picker/image_picker_android/android/src/test/resources/pngImage.png diff --git a/packages/image_picker/image_picker_android/example/README.md b/packages/image_picker/image_picker_android/example/README.md new file mode 100755 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/image_picker/image_picker_android/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/image_picker/image_picker_android/example/android/app/build.gradle b/packages/image_picker/image_picker_android/example/android/app/build.gradle new file mode 100755 index 000000000000..31d8c82a0a9d --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/build.gradle @@ -0,0 +1,67 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + testOptions.unitTests.includeAndroidResources = true + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.imagepicker.example" + minSdkVersion 16 + targetSdkVersion 28 + multiDexEnabled true + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } + + testOptions { + unitTests.returnDefaultValues = true + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.4.0' +} diff --git a/packages/image_picker/image_picker_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker_android/example/android/app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..29e413457635 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -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. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java new file mode 100644 index 000000000000..91e068fa8043 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepickerexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java similarity index 100% rename from packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java rename to packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java 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 new file mode 100644 index 000000000000..6f85cefded34 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/AndroidManifest.xml b/packages/image_picker/image_picker_android/example/android/app/src/main/AndroidManifest.xml new file mode 100755 index 000000000000..543fca922e1b --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/.gitignore b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/.gitignore new file mode 100755 index 000000000000..9eb4563d2ae1 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/.gitignore @@ -0,0 +1 @@ +GeneratedPluginRegistrant.java diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/ImagePickerTestActivity.java b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/ImagePickerTestActivity.java new file mode 100644 index 000000000000..827687a10e79 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/ImagePickerTestActivity.java @@ -0,0 +1,20 @@ +// 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; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class ImagePickerTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-mdpi/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/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 000000000000..09d4391482be Binary files /dev/null and b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/image_picker/image_picker_android/example/android/build.gradle b/packages/image_picker/image_picker_android/example/android/build.gradle new file mode 100755 index 000000000000..e29a4431f2ae --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/image_picker/image_picker_android/example/android/gradle.properties b/packages/image_picker/image_picker_android/example/android/gradle.properties new file mode 100755 index 000000000000..94adc3a3f97a --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/image_picker/image_picker_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..cb24abda10ae --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/packages/image_picker/image_picker_android/example/android/settings.gradle b/packages/image_picker/image_picker_android/example/android/settings.gradle new file mode 100755 index 000000000000..115da6cb4f4d --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withInputStream { stream -> plugins.load(stream) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/packages/image_picker/image_picker_android/example/integration_test/image_picker_test.dart b/packages/image_picker/image_picker_android/example/integration_test/image_picker_test.dart new file mode 100644 index 000000000000..2b82b4bda5e4 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/integration_test/image_picker_test.dart @@ -0,0 +1,12 @@ +// 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:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('placeholder test', (WidgetTester tester) async {}); +} diff --git a/packages/image_picker/image_picker_android/example/lib/main.dart b/packages/image_picker/image_picker_android/example/lib/main.dart new file mode 100755 index 000000000000..212e064cc6e5 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/lib/main.dart @@ -0,0 +1,473 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Image Picker Demo', + home: MyHomePage(title: 'Image Picker Example'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, this.title}) : super(key: key); + + final String? title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + List? _imageFileList; + + void _setImageFileListFromFile(XFile? value) { + _imageFileList = value == null ? null : [value]; + } + + dynamic _pickImageError; + bool isVideo = false; + + VideoPlayerController? _controller; + VideoPlayerController? _toBeDisposed; + String? _retrieveDataError; + + final ImagePickerPlatform _picker = ImagePickerPlatform.instance; + final TextEditingController maxWidthController = TextEditingController(); + final TextEditingController maxHeightController = TextEditingController(); + final TextEditingController qualityController = TextEditingController(); + + Future _playVideo(XFile? file) async { + if (file != null && mounted) { + await _disposeVideoController(); + late VideoPlayerController controller; + if (kIsWeb) { + controller = VideoPlayerController.network(file.path); + } else { + controller = VideoPlayerController.file(File(file.path)); + } + _controller = controller; + // In web, most browsers won't honor a programmatic call to .play + // if the video has a sound track (and is not muted). + // Mute the video so it auto-plays in web! + // This is not needed if the call to .play is the result of user + // interaction (clicking on a "play" button, for example). + const double volume = kIsWeb ? 0.0 : 1.0; + await controller.setVolume(volume); + await controller.initialize(); + await controller.setLooping(true); + await controller.play(); + setState(() {}); + } + } + + Future _onImageButtonPressed(ImageSource source, + {BuildContext? context, 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)); + await _playVideo(file); + } else if (isMultiImage) { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List? pickedFileList = await _picker.getMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + } + + @override + void deactivate() { + if (_controller != null) { + _controller!.setVolume(0.0); + _controller!.pause(); + } + super.deactivate(); + } + + @override + void dispose() { + _disposeVideoController(); + maxWidthController.dispose(); + maxHeightController.dispose(); + qualityController.dispose(); + super.dispose(); + } + + Future _disposeVideoController() async { + if (_toBeDisposed != null) { + await _toBeDisposed!.dispose(); + } + _toBeDisposed = _controller; + _controller = null; + } + + Widget _previewVideo() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_controller == null) { + return const Text( + 'You have not yet picked a video', + textAlign: TextAlign.center, + ); + } + return Padding( + padding: const EdgeInsets.all(10.0), + child: AspectRatioVideo(_controller), + ); + } + + Widget _previewImages() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_imageFileList != null) { + return Semantics( + label: 'image_picker_example_picked_images', + child: ListView.builder( + key: UniqueKey(), + itemBuilder: (BuildContext context, int index) { + // 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)), + ); + }, + itemCount: _imageFileList!.length, + ), + ); + } else if (_pickImageError != null) { + return Text( + 'Pick image error: $_pickImageError', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + + Widget _handlePreview() { + if (isVideo) { + return _previewVideo(); + } else { + return _previewImages(); + } + } + + Future retrieveLostData() async { + final LostDataResponse response = await _picker.getLostData(); + if (response.isEmpty) { + return; + } + if (response.file != null) { + if (response.type == RetrieveType.video) { + isVideo = true; + await _playVideo(response.file); + } else { + isVideo = false; + setState(() { + if (response.files == null) { + _setImageFileListFromFile(response.file); + } else { + _imageFileList = response.files; + } + }); + } + } else { + _retrieveDataError = response.exception!.code; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title!), + ), + body: Center( + child: !kIsWeb && defaultTargetPlatform == TargetPlatform.android + ? FutureBuilder( + future: retrieveLostData(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + case ConnectionState.done: + return _handlePreview(); + default: + if (snapshot.hasError) { + return Text( + 'Pick image/video error: ${snapshot.error}}', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + }, + ) + : _handlePreview(), + ), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Semantics( + label: 'image_picker_example_from_gallery', + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'image0', + tooltip: 'Pick Image from gallery', + child: const Icon(Icons.photo), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); + }, + heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + isVideo = true; + _onImageButtonPressed(ImageSource.gallery); + }, + heroTag: 'video0', + tooltip: 'Pick Video from gallery', + child: const Icon(Icons.video_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + isVideo = true; + _onImageButtonPressed(ImageSource.camera); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), + ), + ], + ), + ); + } + + Text? _getRetrieveErrorWidget() { + if (_retrieveDataError != null) { + final Text result = Text(_retrieveDataError!); + _retrieveDataError = null; + return result; + } + return null; + } + + Future _displayPickImageDialog( + BuildContext context, OnPickImageCallback onPick) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Add optional parameters'), + content: Column( + children: [ + TextField( + controller: maxWidthController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxWidth if desired'), + ), + TextField( + controller: maxHeightController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxHeight if desired'), + ), + TextField( + controller: qualityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter quality if desired'), + ), + ], + ), + actions: [ + TextButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('PICK'), + onPressed: () { + final double? width = maxWidthController.text.isNotEmpty + ? double.parse(maxWidthController.text) + : null; + final double? height = maxHeightController.text.isNotEmpty + ? double.parse(maxHeightController.text) + : null; + final int? quality = qualityController.text.isNotEmpty + ? int.parse(qualityController.text) + : null; + onPick(width, height, quality); + Navigator.of(context).pop(); + }), + ], + ); + }); + } +} + +typedef OnPickImageCallback = void Function( + double? maxWidth, double? maxHeight, int? quality); + +class AspectRatioVideo extends StatefulWidget { + const AspectRatioVideo(this.controller, {Key? key}) : super(key: key); + + final VideoPlayerController? controller; + + @override + AspectRatioVideoState createState() => AspectRatioVideoState(); +} + +class AspectRatioVideoState extends State { + VideoPlayerController? get controller => widget.controller; + bool initialized = false; + + void _onVideoControllerUpdate() { + if (!mounted) { + return; + } + if (initialized != controller!.value.isInitialized) { + initialized = controller!.value.isInitialized; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + controller!.addListener(_onVideoControllerUpdate); + } + + @override + void dispose() { + controller!.removeListener(_onVideoControllerUpdate); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (initialized) { + return Center( + child: AspectRatio( + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller!), + ), + ); + } else { + return Container(); + } + } +} diff --git a/packages/image_picker/image_picker_android/example/pubspec.yaml b/packages/image_picker/image_picker_android/example/pubspec.yaml new file mode 100755 index 000000000000..02ef8a02af4c --- /dev/null +++ b/packages/image_picker/image_picker_android/example/pubspec.yaml @@ -0,0 +1,33 @@ +name: image_picker_example +description: Demonstrates how to use the image_picker plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.1 + image_picker_android: + # When depending on this package from a real application you should use: + # image_picker_android: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + image_picker_platform_interface: ^2.3.0 + video_player: ^2.1.4 + +dev_dependencies: + espresso: ^0.2.0 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/image_picker/image_picker_android/example/test_driver/integration_test.dart b/packages/image_picker/image_picker_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/test_driver/integration_test.dart @@ -0,0 +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. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/image_picker/image_picker_android/lib/image_picker_android.dart b/packages/image_picker/image_picker_android/lib/image_picker_android.dart new file mode 100644 index 000000000000..b6073c7a436a --- /dev/null +++ b/packages/image_picker/image_picker_android/lib/image_picker_android.dart @@ -0,0 +1,282 @@ +// 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:flutter/services.dart'; + +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/image_picker_android'); + +/// An Android implementation of [ImagePickerPlatform]. +class ImagePickerAndroid extends ImagePickerPlatform { + /// The MethodChannel that is being used by this implementation of the plugin. + @visibleForTesting + MethodChannel get channel => _channel; + + /// Registers this class as the default platform implementation. + static void registerWith() { + ImagePickerPlatform.instance = ImagePickerAndroid(); + } + + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final String? path = await _getImagePath( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? PickedFile(path) : null; + } + + @override + Future?> pickMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + final List? paths = await _getMultiImagePath( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); + if (paths == null) { + return null; + } + + return paths.map((dynamic path) => PickedFile(path as String)).toList(); + } + + Future?> _getMultiImagePath({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) { + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + + return _channel.invokeMethod?>( + 'pickMultiImage', + { + 'maxWidth': maxWidth, + 'maxHeight': maxHeight, + 'imageQuality': imageQuality, + }, + ); + } + + Future _getImagePath({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + bool requestFullMetadata = true, + }) { + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + + return _channel.invokeMethod( + 'pickImage', + { + 'source': source.index, + 'maxWidth': maxWidth, + 'maxHeight': maxHeight, + 'imageQuality': imageQuality, + 'cameraDevice': preferredCameraDevice.index, + 'requestFullMetadata': requestFullMetadata, + }, + ); + } + + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final String? path = await _getVideoPath( + source: source, + maxDuration: maxDuration, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? PickedFile(path) : null; + } + + Future _getVideoPath({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) { + return _channel.invokeMethod( + 'pickVideo', + { + 'source': source.index, + 'maxDuration': maxDuration?.inSeconds, + 'cameraDevice': preferredCameraDevice.index + }, + ); + } + + @override + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final String? path = await _getImagePath( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? XFile(path) : null; + } + + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + final String? path = await _getImagePath( + source: source, + maxHeight: options.maxHeight, + maxWidth: options.maxWidth, + imageQuality: options.imageQuality, + preferredCameraDevice: options.preferredCameraDevice, + requestFullMetadata: options.requestFullMetadata, + ); + return path != null ? XFile(path) : null; + } + + @override + Future?> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + final List? paths = await _getMultiImagePath( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); + if (paths == null) { + return null; + } + + return paths.map((dynamic path) => XFile(path as String)).toList(); + } + + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final String? path = await _getVideoPath( + source: source, + maxDuration: maxDuration, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? XFile(path) : null; + } + + @override + Future retrieveLostData() async { + final LostDataResponse result = await getLostData(); + + if (result.isEmpty) { + return LostData.empty(); + } + + return LostData( + file: result.file != null ? PickedFile(result.file!.path) : null, + exception: result.exception, + type: result.type, + ); + } + + @override + Future getLostData() async { + List? pickedFileList; + + final Map? result = + await _channel.invokeMapMethod('retrieve'); + + if (result == null) { + return LostDataResponse.empty(); + } + + assert(result.containsKey('path') != result.containsKey('errorCode')); + + final String? type = result['type'] as String?; + assert(type == kTypeImage || type == kTypeVideo); + + RetrieveType? retrieveType; + if (type == kTypeImage) { + retrieveType = RetrieveType.image; + } else if (type == kTypeVideo) { + retrieveType = RetrieveType.video; + } + + PlatformException? exception; + if (result.containsKey('errorCode')) { + exception = PlatformException( + code: result['errorCode']! as String, + message: result['errorMessage'] as String?); + } + + final String? path = result['path'] as String?; + + final List? pathList = + (result['pathList'] as List?)?.cast(); + if (pathList != null) { + pickedFileList = []; + for (final String path in pathList) { + pickedFileList.add(XFile(path)); + } + } + + return LostDataResponse( + file: path != null ? XFile(path) : null, + exception: exception, + type: retrieveType, + files: pickedFileList, + ); + } +} diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml new file mode 100755 index 000000000000..c0092493b715 --- /dev/null +++ b/packages/image_picker/image_picker_android/pubspec.yaml @@ -0,0 +1,29 @@ +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+3 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: image_picker + platforms: + android: + package: io.flutter.plugins.imagepicker + pluginClass: ImagePickerPlugin + dartPluginClass: ImagePickerAndroid + +dependencies: + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.1 + image_picker_platform_interface: ^2.5.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.0 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 new file mode 100644 index 000000000000..ee1eb79f1045 --- /dev/null +++ b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart @@ -0,0 +1,1256 @@ +// 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'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:image_picker_android/image_picker_android.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final ImagePickerAndroid picker = ImagePickerAndroid(); + + final List log = []; + dynamic returnValue = ''; + + setUp(() { + returnValue = ''; + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return returnValue; + }); + + log.clear(); + }); + + test('registers instance', () async { + ImagePickerAndroid.registerWith(); + expect(ImagePickerPlatform.instance, isA()); + }); + + group('#pickImage', () { + test('passes the image source argument correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickImage( + source: ImageSource.camera, + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('does not accept an invalid imageQuality argument', () { + expect( + () => picker.pickImage(imageQuality: -1, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: 101, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: -1, source: ImageSource.camera), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: 101, source: ImageSource.camera), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.pickImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.pickImage(source: ImageSource.gallery), isNull); + expect(await picker.pickImage(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.pickImage(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.pickImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + 'requestFullMetadata': true, + }), + ], + ); + }); + }); + + group('#pickMultiImage', () { + test('calls the method correctly', () async { + returnValue = ['0', '1']; + await picker.pickMultiImage(); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + returnValue = ['0', '1']; + await picker.pickMultiImage(); + await picker.pickMultiImage( + maxWidth: 10.0, + ); + await picker.pickMultiImage( + maxHeight: 10.0, + ); + await picker.pickMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickMultiImage( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickMultiImage( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + returnValue = ['0', '1']; + expect( + () => picker.pickMultiImage(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickMultiImage(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('does not accept an invalid imageQuality argument', () { + returnValue = ['0', '1']; + expect( + () => picker.pickMultiImage(imageQuality: -1), + throwsArgumentError, + ); + + expect( + () => picker.pickMultiImage(imageQuality: 101), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.pickMultiImage(), isNull); + expect(await picker.pickMultiImage(), isNull); + }); + }); + + group('#pickVideo', () { + test('passes the image source argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + isMethodCall('pickVideo', arguments: { + 'source': 1, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10), + ); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1), + ); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1), + ); + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 10, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 60, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 3600, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.pickVideo(source: ImageSource.gallery), isNull); + expect(await picker.pickVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.pickVideo(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.pickVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front, + ); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#retrieveLostData', () { + test('retrieveLostData get success response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path', + }; + }); + final LostData response = await picker.retrieveLostData(); + expect(response.type, RetrieveType.image); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path'); + }); + + test('retrieveLostData get error response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + }; + }); + final LostData response = await picker.retrieveLostData(); + expect(response.type, RetrieveType.video); + expect(response.exception, isNotNull); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); + }); + + test('retrieveLostData get null response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return null; + }); + expect((await picker.retrieveLostData()).isEmpty, true); + }); + + test('retrieveLostData get both path and error should throw', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + 'path': '/example/path', + }; + }); + expect(picker.retrieveLostData(), throwsAssertionError); + }); + }); + + group('#getImage', () { + test('passes the image source argument correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.getImage( + source: ImageSource.camera, + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('does not accept an invalid imageQuality argument', () { + expect( + () => picker.getImage(imageQuality: -1, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: 101, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: -1, source: ImageSource.camera), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: 101, source: ImageSource.camera), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.getImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.getImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getImage(source: ImageSource.gallery), isNull); + expect(await picker.getImage(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getImage(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + 'requestFullMetadata': true, + }), + ], + ); + }); + }); + + group('#getMultiImage', () { + test('calls the method correctly', () async { + returnValue = ['0', '1']; + await picker.getMultiImage(); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + returnValue = ['0', '1']; + await picker.getMultiImage(); + await picker.getMultiImage( + maxWidth: 10.0, + ); + await picker.getMultiImage( + maxHeight: 10.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMultiImage(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImage(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('does not accept an invalid imageQuality argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMultiImage(imageQuality: -1), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImage(imageQuality: 101), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getMultiImage(), isNull); + expect(await picker.getMultiImage(), isNull); + }); + }); + + group('#getVideo', () { + test('passes the image source argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + isMethodCall('pickVideo', arguments: { + 'source': 1, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10), + ); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1), + ); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1), + ); + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 10, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 60, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 3600, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getVideo(source: ImageSource.gallery), isNull); + expect(await picker.getVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getVideo(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front, + ); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#getLostData', () { + test('getLostData get success response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path', + }; + }); + final LostDataResponse response = await picker.getLostData(); + expect(response.type, RetrieveType.image); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path'); + }); + + test('getLostData should successfully retrieve multiple files', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path1', + 'pathList': ['/example/path0', '/example/path1'], + }; + }); + final LostDataResponse response = await picker.getLostData(); + expect(response.type, RetrieveType.image); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path1'); + expect(response.files!.first.path, '/example/path0'); + expect(response.files!.length, 2); + }); + + test('getLostData get error response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + }; + }); + final LostDataResponse response = await picker.getLostData(); + expect(response.type, RetrieveType.video); + expect(response.exception, isNotNull); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); + }); + + test('getLostData get null response', () async { + picker.channel.setMockMethodCallHandler((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 { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + 'path': '/example/path', + }; + }); + expect(picker.getLostData(), throwsAssertionError); + }); + }); + + group('#getImageFromSource', () { + test('passes the image source argument correctly', () async { + await picker.getImageFromSource(source: ImageSource.camera); + await picker.getImageFromSource(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.getImageFromSource(source: ImageSource.camera); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxWidth: 10.0), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxHeight: 10.0), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxWidth: 10.0, + maxHeight: 20.0, + ), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxWidth: 10.0, + imageQuality: 70, + ), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxHeight: 10.0, + imageQuality: 70, + ), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('does not accept an invalid imageQuality argument', () { + expect( + () => picker.getImageFromSource( + source: ImageSource.gallery, + options: const ImagePickerOptions(imageQuality: -1), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.gallery, + options: const ImagePickerOptions(imageQuality: 101), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(imageQuality: -1), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(imageQuality: 101), + ), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxWidth: -1.0), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxHeight: -1.0), + ), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect( + await picker.getImageFromSource(source: ImageSource.gallery), isNull); + expect( + await picker.getImageFromSource(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getImageFromSource(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + preferredCameraDevice: CameraDevice.front, + ), + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the full metadata argument correctly', () async { + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(requestFullMetadata: false), + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': false, + }), + ], + ); + }); + }); +} diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index dcf353fe19b1..8a5c089ef807 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,3 +1,22 @@ +## 2.1.10 + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. + +## 2.1.9 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. +* Fixes violations of new analysis option use_named_constants. + +## 2.1.8 + +* Minor fixes for new analysis options. + +## 2.1.7 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.1.6 * Internal code cleanup for stricter analysis options. diff --git a/packages/image_picker/image_picker_for_web/example/README.md b/packages/image_picker/image_picker_for_web/example/README.md index 4348451b14e2..0e51ae5ecbd2 100644 --- a/packages/image_picker/image_picker_for_web/example/README.md +++ b/packages/image_picker/image_picker_for_web/example/README.md @@ -1,4 +1,14 @@ -# Testing +# 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. + +## Testing This package uses `package:integration_test` to run its tests in a web browser. diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart index 91794a7d5e78..0ff6d2380004 100644 --- a/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart +++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart @@ -33,8 +33,8 @@ void main() { testWidgets('image is loaded correctly ', (WidgetTester tester) async { final html.ImageElement imageElement = await imageResizer.loadImage(pngFile.path); - expect(imageElement.width!, 10); - expect(imageElement.height!, 10); + expect(imageElement.width, 10); + expect(imageElement.height, 10); }); testWidgets( @@ -117,7 +117,7 @@ Future _getImageSize(XFile file) async { completer.complete(Size(image.width!.toDouble(), image.height!.toDouble())); }); image.onError.listen((html.Event event) { - completer.complete(const Size(0, 0)); + completer.complete(Size.zero); }); return completer.future; } diff --git a/packages/image_picker/image_picker_for_web/example/lib/main.dart b/packages/image_picker/image_picker_for_web/example/lib/main.dart index 341913a18490..87422953de6a 100644 --- a/packages/image_picker/image_picker_for_web/example/lib/main.dart +++ b/packages/image_picker/image_picker_for_web/example/lib/main.dart @@ -5,13 +5,16 @@ import 'package:flutter/material.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } /// App for testing class MyApp extends StatefulWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { 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 a9d6c7b9b5bd..c39bd81f9de0 100644 --- a/packages/image_picker/image_picker_for_web/example/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml @@ -3,13 +3,14 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.2.0" + flutter: ">=2.10.0" dependencies: flutter: sdk: flutter image_picker_for_web: path: ../ + image_picker_platform_interface: ^2.2.0 dev_dependencies: flutter_driver: diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index 88d439c5487f..bb261f76f320 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -7,9 +7,10 @@ import 'dart:html' as html; import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -import 'package:image_picker_for_web/src/image_resizer.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'src/image_resizer.dart'; + const String _kImagePickerInputsDomId = '__image_picker_web-file-input'; const String _kAcceptImageMimeType = 'image/*'; const String _kAcceptVideoMimeType = 'video/3gpp,video/x-m4v,video/mp4,video/*'; @@ -243,35 +244,35 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Monitors an and returns the selected file. Future _getSelectedFile(html.FileUploadInputElement input) { - final Completer _completer = Completer(); + final Completer completer = Completer(); // Observe the input until we can return something input.onChange.first.then((html.Event event) { final List? files = _handleOnChangeEvent(event); - if (!_completer.isCompleted && files != null) { - _completer.complete(PickedFile( + if (!completer.isCompleted && files != null) { + completer.complete(PickedFile( html.Url.createObjectUrl(files.first), )); } }); input.onError.first.then((html.Event event) { - if (!_completer.isCompleted) { - _completer.completeError(event); + if (!completer.isCompleted) { + completer.completeError(event); } }); // Note that we don't bother detaching from these streams, since the // "input" gets re-created in the DOM every time the user needs to // pick a file. - return _completer.future; + return completer.future; } /// Monitors an and returns the selected file(s). Future> _getSelectedXFiles(html.FileUploadInputElement input) { - final Completer> _completer = Completer>(); + final Completer> completer = Completer>(); // Observe the input until we can return something input.onChange.first.then((html.Event event) { final List? files = _handleOnChangeEvent(event); - if (!_completer.isCompleted && files != null) { - _completer.complete(files.map((html.File file) { + if (!completer.isCompleted && files != null) { + completer.complete(files.map((html.File file) { return XFile( html.Url.createObjectUrl(file), name: file.name, @@ -285,14 +286,14 @@ class ImagePickerPlugin extends ImagePickerPlatform { } }); input.onError.first.then((html.Event event) { - if (!_completer.isCompleted) { - _completer.completeError(event); + if (!completer.isCompleted) { + completer.completeError(event); } }); // Note that we don't bother detaching from these streams, since the // "input" gets re-created in the DOM every time the user needs to // pick a file. - return _completer.future; + return completer.future; } /// Initializes a DOM container where we can host input elements. diff --git a/packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart b/packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart index e063099e3319..7cca935c6c91 100644 --- a/packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart +++ b/packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart @@ -7,9 +7,10 @@ import 'dart:html' as html; import 'dart:math'; import 'dart:ui'; -import 'package:image_picker_for_web/src/image_resizer_utils.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'image_resizer_utils.dart'; + /// Helper class that resizes images. class ImageResizer { /// Resizes the image if needed. @@ -39,6 +40,7 @@ class ImageResizer { final Completer imageLoadCompleter = Completer(); final html.ImageElement imageElement = html.ImageElement(); + // ignore: unsafe_html imageElement.src = blobUrl; imageElement.onLoad.listen((html.Event event) { @@ -81,7 +83,7 @@ class ImageResizer { await canvas.toBlob(originalFile.mimeType, calculatedImageQuality); return XFile(html.Url.createObjectUrlFromBlob(blob), mimeType: originalFile.mimeType, - name: 'scaled_' + originalFile.name, + name: 'scaled_${originalFile.name}', lastModified: DateTime.now(), length: blob.size); } diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index deccd2b50a1f..c2e0975dda57 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -2,11 +2,11 @@ name: image_picker_for_web description: Web platform implementation of image_picker repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker_for_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 2.1.6 +version: 2.1.10 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/image_picker/image_picker_ios/AUTHORS b/packages/image_picker/image_picker_ios/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/image_picker/image_picker_ios/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md new file mode 100644 index 000000000000..986f5c0ff6ca --- /dev/null +++ b/packages/image_picker/image_picker_ios/CHANGELOG.md @@ -0,0 +1,45 @@ +## 0.8.6+1 + +* Fixes issue with crashing the app when picking images with PHPicker without providing `Photo Library Usage` permission. + +## 0.8.6 + +* Adds `requestFullMetadata` option to `pickImage`, so images on iOS can be picked without `Photo Library Usage` permission. +* Updates minimum Flutter version to 2.10. + +## 0.8.5+6 + +* Updates description. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). + +## 0.8.5+5 + +* Adds non-deprecated codepaths for iOS 13+. + +## 0.8.5+4 + +* Suppresses warnings for pre-iOS-11 codepaths. + +## 0.8.5+3 + +* Fixes 'messages.g.h' file not found. + +## 0.8.5+2 + +* Minor fixes for new analysis options. + +## 0.8.5+1 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.8.5 + +* Switches to an in-package method channel based on Pigeon. +* Fixes invalid casts when selecting multiple images on versions of iOS before + 14.0. + +## 0.8.4+11 + +* Splits from `image_picker` as a federated implementation. diff --git a/packages/image_picker/image_picker_ios/LICENSE b/packages/image_picker/image_picker_ios/LICENSE new file mode 100644 index 000000000000..0be8bbc3e68d --- /dev/null +++ b/packages/image_picker/image_picker_ios/LICENSE @@ -0,0 +1,231 @@ +image_picker + +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. +-------------------------------------------------------------------------------- +aFileChooser + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2011 - 2013 Paul Burke + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/image_picker/image_picker_ios/README.md b/packages/image_picker/image_picker_ios/README.md new file mode 100755 index 000000000000..e9fc2cfe61e7 --- /dev/null +++ b/packages/image_picker/image_picker_ios/README.md @@ -0,0 +1,11 @@ +# image\_picker\_ios + +The iOS implementation of [`image_picker`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `image_picker` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/image_picker +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/image_picker/image_picker_ios/example/README.md b/packages/image_picker/image_picker_ios/example/README.md new file mode 100755 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/image_picker/image_picker_ios/example/integration_test/image_picker_test.dart b/packages/image_picker/image_picker_ios/example/integration_test/image_picker_test.dart new file mode 100644 index 000000000000..2b82b4bda5e4 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/integration_test/image_picker_test.dart @@ -0,0 +1,12 @@ +// 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:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('placeholder test', (WidgetTester tester) async {}); +} diff --git a/packages/image_picker/image_picker_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/image_picker/image_picker_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100755 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 9.0 + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Flutter/Debug.xcconfig b/packages/image_picker/image_picker_ios/example/ios/Flutter/Debug.xcconfig new file mode 100755 index 000000000000..9803018ca79d --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Generated.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/image_picker/image_picker_ios/example/ios/Flutter/Release.xcconfig b/packages/image_picker/image_picker_ios/example/ios/Flutter/Release.xcconfig new file mode 100755 index 000000000000..a4a8c604e13d --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Generated.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/image_picker/image_picker_ios/example/ios/Podfile b/packages/image_picker/image_picker_ios/example/ios/Podfile new file mode 100644 index 000000000000..5bc7b7e85717 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Podfile @@ -0,0 +1,45 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + target 'RunnerTests' do + platform :ios, '9.0' + inherit! :search_paths + # Pods for testing + pod 'OCMock', '~> 3.8.1' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end 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 new file mode 100644 index 000000000000..2847bfd85046 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,796 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 334733FC266813EE00DCC49E /* ImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */; }; + 334733FD266813F100DCC49E /* MetaDataUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 680049252280D736006DD6AB /* MetaDataUtilTests.m */; }; + 334733FE266813F400DCC49E /* PhotoAssetUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */; }; + 334733FF266813FA00DCC49E /* ImagePickerTestImages.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */; }; + 33473400266813FD00DCC49E /* ImagePickerPluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */; }; + 3A72BAD3FAE6E0FA9D80826B /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 35AE65F25E0B8C8214D8372B /* libPods-RunnerTests.a */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 5C9513011EC38BD300040975 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */; }; + 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 */; }; + 86430DF9272D71E9002D9D6C /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; + 86E9A893272754860017E6E0 /* PickerSaveImageToPathOperationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */; }; + 86E9A894272754A30017E6E0 /* webpImage.webp in Resources */ = {isa = PBXBuildFile; fileRef = 86E9A88F272747B90017E6E0 /* webpImage.webp */; }; + 86E9A895272769130017E6E0 /* pngImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 680049352280F2B8006DD6AB /* pngImage.png */; }; + 86E9A896272769150017E6E0 /* jpgImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 680049362280F2B8006DD6AB /* jpgImage.jpg */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9FC8F0E9229FA49E00C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; + 9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; + BE6173D826A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */; }; + F4F7A436CCA4BF276270A3AE /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EC32F6993F4529982D9519F1 /* libPods-Runner.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 334733F72668136400DCC49E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + 6801C83B2555D726009DAF8D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0C7B151765FD4249454C49AD /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 334733F22668136400DCC49E /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 334733F62668136400DCC49E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 35AE65F25E0B8C8214D8372B /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5A9D31B91557877A0E8EF3E7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 5C9512FF1EC38BD300040975 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 680049252280D736006DD6AB /* MetaDataUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MetaDataUtilTests.m; sourceTree = ""; }; + 680049352280F2B8006DD6AB /* pngImage.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = pngImage.png; sourceTree = ""; }; + 680049362280F2B8006DD6AB /* jpgImage.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = jpgImage.jpg; sourceTree = ""; }; + 6801632E632668F4349764C9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 6801C8362555D726009DAF8D /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromGalleryUITests.m; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + 86E9A88F272747B90017E6E0 /* webpImage.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = webpImage.webp; sourceTree = ""; }; + 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PickerSaveImageToPathOperationTests.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = gifImage.gif; sourceTree = ""; }; + 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImageUtilTests.m; sourceTree = ""; }; + BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromLimitedGalleryUITests.m; sourceTree = ""; }; + DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + EC32F6993F4529982D9519F1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ImagePickerTestImages.h; sourceTree = ""; }; + F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerTestImages.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 334733EF2668136400DCC49E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A72BAD3FAE6E0FA9D80826B /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6801C8332555D726009DAF8D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F4F7A436CCA4BF276270A3AE /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 334733F32668136400DCC49E /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */, + 680049252280D736006DD6AB /* MetaDataUtilTests.m */, + 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */, + F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */, + F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */, + 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */, + 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */, + 334733F62668136400DCC49E /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 680049282280E33D006DD6AB /* TestImages */ = { + isa = PBXGroup; + children = ( + 86E9A88F272747B90017E6E0 /* webpImage.webp */, + 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */, + 680049362280F2B8006DD6AB /* jpgImage.jpg */, + 680049352280F2B8006DD6AB /* pngImage.png */, + ); + path = TestImages; + sourceTree = ""; + }; + 6801C8372555D726009DAF8D /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */, + 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */, + 6801C83A2555D726009DAF8D /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; + 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { + isa = PBXGroup; + children = ( + 6801632E632668F4349764C9 /* Pods-Runner.debug.xcconfig */, + 5A9D31B91557877A0E8EF3E7 /* Pods-Runner.release.xcconfig */, + DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */, + 0C7B151765FD4249454C49AD /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 680049282280E33D006DD6AB /* TestImages */, + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 334733F32668136400DCC49E /* RunnerTests */, + 6801C8372555D726009DAF8D /* RunnerUITests */, + 97C146EF1CF9000F007C117D /* Products */, + 840012C8B5EDBCF56B0E4AC1 /* Pods */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 6801C8362555D726009DAF8D /* RunnerUITests.xctest */, + 334733F22668136400DCC49E /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 5C9512FF1EC38BD300040975 /* GeneratedPluginRegistrant.h */, + 5C9513001EC38BD300040975 /* GeneratedPluginRegistrant.m */, + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { + isa = PBXGroup; + children = ( + EC32F6993F4529982D9519F1 /* libPods-Runner.a */, + 35AE65F25E0B8C8214D8372B /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 334733F12668136400DCC49E /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 334733F92668136400DCC49E /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + B8739A4353234497CF76B597 /* [CP] Check Pods Manifest.lock */, + 334733EE2668136400DCC49E /* Sources */, + 334733EF2668136400DCC49E /* Frameworks */, + 334733F02668136400DCC49E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 334733F82668136400DCC49E /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 334733F22668136400DCC49E /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 6801C8352555D726009DAF8D /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6801C83F2555D726009DAF8D /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + 6801C8322555D726009DAF8D /* Sources */, + 6801C8332555D726009DAF8D /* Frameworks */, + 6801C8342555D726009DAF8D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6801C83C2555D726009DAF8D /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = 6801C8362555D726009DAF8D /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Original; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 334733F12668136400DCC49E = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 6801C8352555D726009DAF8D = { + CreatedOnToolsVersion = 11.7; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 1; + }; + }; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 334733F12668136400DCC49E /* RunnerTests */, + 6801C8352555D726009DAF8D /* RunnerUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 334733F02668136400DCC49E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 86430DF9272D71E9002D9D6C /* gifImage.gif in Resources */, + 86E9A894272754A30017E6E0 /* webpImage.webp in Resources */, + 86E9A895272769130017E6E0 /* pngImage.png in Resources */, + 86E9A896272769150017E6E0 /* jpgImage.jpg in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6801C8342555D726009DAF8D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */, + 680049382280F2B9006DD6AB /* pngImage.png in Resources */, + 680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 9FC8F0E9229FA49E00C8D58F /* gifImage.gif in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + B8739A4353234497CF76B597 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 334733EE2668136400DCC49E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 334733FD266813F100DCC49E /* MetaDataUtilTests.m in Sources */, + 334733FF266813FA00DCC49E /* ImagePickerTestImages.m in Sources */, + 86E9A893272754860017E6E0 /* PickerSaveImageToPathOperationTests.m in Sources */, + 334733FC266813EE00DCC49E /* ImageUtilTests.m in Sources */, + 33473400266813FD00DCC49E /* ImagePickerPluginTests.m in Sources */, + 334733FE266813F400DCC49E /* PhotoAssetUtilTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6801C8322555D726009DAF8D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6801C8392555D726009DAF8D /* ImagePickerFromGalleryUITests.m in Sources */, + BE6173D826A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 5C9513011EC38BD300040975 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 334733F82668136400DCC49E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 334733F72668136400DCC49E /* PBXContainerItemProxy */; + }; + 6801C83C2555D726009DAF8D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 6801C83B2555D726009DAF8D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 334733FA2668136400DCC49E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + 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"; + }; + name = Debug; + }; + 334733FB2668136400DCC49E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0C7B151765FD4249454C49AD /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + 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"; + }; + name = Release; + }; + 6801C83D2555D726009DAF8D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + 6801C83E2555D726009DAF8D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.imagePickerExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.imagePickerExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 334733F92668136400DCC49E /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 334733FA2668136400DCC49E /* Debug */, + 334733FB2668136400DCC49E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6801C83F2555D726009DAF8D /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6801C83D2555D726009DAF8D /* Debug */, + 6801C83E2555D726009DAF8D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100755 index 000000000000..919434a6254f --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100755 index 000000000000..9b24f28c25cc --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme new file mode 100644 index 000000000000..1a97d9638346 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100755 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/.gitignore b/packages/image_picker/image_picker_ios/example/ios/Runner/.gitignore new file mode 100755 index 000000000000..0cab08d0bdd7 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/.gitignore @@ -0,0 +1,2 @@ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.h b/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.h @@ -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 +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.m b/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..b790a0a52635 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.m @@ -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. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 000000000000..d225b3c2cfe2 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,121 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100755 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100755 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100755 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100755 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100755 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100755 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100755 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100755 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100755 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100755 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100755 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100755 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100755 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100755 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/Contents.json b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/Contents.json new file mode 100644 index 000000000000..da4a164c9186 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100755 index 000000000000..ebf48f603974 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100755 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist b/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist new file mode 100755 index 000000000000..f9c1909383ca --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist @@ -0,0 +1,59 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + image_picker_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSCameraUsageDescription + Used to demonstrate image picker plugin + NSMicrophoneUsageDescription + Used to capture audio for image picker plugin + NSPhotoLibraryUsageDescription + Used to demonstrate image picker plugin + UIBackgroundModes + + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/main.m b/packages/image_picker/image_picker_ios/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/main.m @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m similarity index 55% rename from packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m index 5f3287400c5e..320582b0f8a3 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m @@ -4,8 +4,8 @@ #import "ImagePickerTestImages.h" -@import image_picker; -@import image_picker.Test; +@import image_picker_ios; +@import image_picker_ios.Test; @import XCTest; #import @@ -46,15 +46,17 @@ - (void)testPluginPickImageDeviceBack { .andReturn(AVAuthorizationStatusAuthorized); // Run test - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + fullMetadata:@(YES) + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceRear); } @@ -77,15 +79,17 @@ - (void)testPluginPickImageDeviceFront { .andReturn(AVAuthorizationStatusAuthorized); // Run test - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraFront] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + fullMetadata:@(YES) + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceFront); } @@ -108,15 +112,15 @@ - (void)testPluginPickVideoDeviceBack { .andReturn(AVAuthorizationStatusAuthorized); // Run test - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxDuration:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceRear); } @@ -140,15 +144,15 @@ - (void)testPluginPickVideoDeviceFront { .andReturn(AVAuthorizationStatusAuthorized); // Run test - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraFront] + maxDuration:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceFront); } @@ -163,41 +167,71 @@ - (void)testPickMultiImageShouldUseUIImagePickerControllerOnPreiOS14 { OCMStub(ClassMethod([photoLibrary authorizationStatus])) .andReturn(PHAuthorizationStatusAuthorized); - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; [plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]]; - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"pickMultiImage" - arguments:@{ - @"maxWidth" : @(100), - @"maxHeight" : @(200), - @"imageQuality" : @(50), - }]; - - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + [plugin pickMultiImageWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] + quality:@(50) + fullMetadata:@(YES) + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; OCMVerify(times(1), [mockUIImagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]); } +- (void)testPickImageWithoutFullMetadata API_AVAILABLE(ios(11)) { + id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + id photoLibrary = OCMClassMock([PHPhotoLibrary class]); + + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + [plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]]; + + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery + camera:FLTSourceCameraFront] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + fullMetadata:@(NO) + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; + + OCMVerify(times(0), [photoLibrary authorizationStatus]); +} + +- (void)testPickMultiImageWithoutFullMetadata API_AVAILABLE(ios(11)) { + id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + id photoLibrary = OCMClassMock([PHPhotoLibrary class]); + + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + [plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]]; + + [plugin pickMultiImageWithMaxSize:[[FLTMaxSize alloc] init] + quality:nil + fullMetadata:@(NO) + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + + OCMVerify(times(0), [photoLibrary authorizationStatus]); +} + #pragma mark - Test camera devices, no op on simulators - (void)testPluginPickImageDeviceCancelClickMultipleTimes { if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { return; } - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; plugin.imagePickerControllerOverrides = @[ controller ]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - plugin.result = ^(id result) { - }; + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + fullMetadata:@(YES) + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; // To ensure the flow does not crash by multiple cancel call [plugin imagePickerControllerDidCancel:controller]; @@ -207,15 +241,16 @@ - (void)testPluginPickImageDeviceCancelClickMultipleTimes { #pragma mark - Test video duration - (void)testPickingVideoWithDuration { - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = [FlutterMethodCall - methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0), @"maxDuration" : @95}]; + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxDuration:@(95) + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; + XCTAssertEqual(controller.videoMaximumDuration, 95); } @@ -227,41 +262,21 @@ - (void)testViewController { UIViewController *vc2 = [UIViewController new]; vc1.mockPresented = vc2; - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; XCTAssertEqual([plugin viewControllerWithWindow:window], vc2); } -- (void)testPluginMultiImagePathIsNil { - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - - dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); - __block FlutterError *pickImageResult = nil; - - plugin.result = ^(id _Nullable r) { - pickImageResult = r; - dispatch_semaphore_signal(resultSemaphore); - }; - [plugin handleSavedPathList:nil]; - - dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); - - XCTAssertEqualObjects(pickImageResult.code, @"create_error"); -} - - (void)testPluginMultiImagePathHasNullItem { - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - NSMutableArray *pathList = [NSMutableArray new]; - - [pathList addObject:[NSNull null]]; + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); __block FlutterError *pickImageResult = nil; - - plugin.result = ^(id _Nullable r) { - pickImageResult = r; - dispatch_semaphore_signal(resultSemaphore); - }; - [plugin handleSavedPathList:pathList]; + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { + pickImageResult = error; + dispatch_semaphore_signal(resultSemaphore); + }]; + [plugin sendCallResultWithSavedPathList:@[ [NSNull null] ]]; dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); @@ -269,20 +284,18 @@ - (void)testPluginMultiImagePathHasNullItem { } - (void)testPluginMultiImagePathHasItem { - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - NSString *savedPath = @"test"; - NSMutableArray *pathList = [NSMutableArray new]; - - [pathList addObject:savedPath]; + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + NSArray *pathList = @[ @"test" ]; dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); __block id pickImageResult = nil; - plugin.result = ^(id _Nullable r) { - pickImageResult = r; - dispatch_semaphore_signal(resultSemaphore); - }; - [plugin handleSavedPathList:pathList]; + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { + pickImageResult = result; + dispatch_semaphore_signal(resultSemaphore); + }]; + [plugin sendCallResultWithSavedPathList:pathList]; dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.h b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerTestImages.h similarity index 100% rename from packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.h rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerTestImages.h diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerTestImages.m similarity index 100% rename from packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerTestImages.m diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/ImageUtilTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m similarity index 97% rename from packages/image_picker/image_picker/example/ios/RunnerTests/ImageUtilTests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m index 9b9719f88116..e449a84b80bb 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/ImageUtilTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m @@ -4,8 +4,8 @@ #import "ImagePickerTestImages.h" -@import image_picker; -@import image_picker.Test; +@import image_picker_ios; +@import image_picker_ios.Test; @import XCTest; @interface ImageUtilTests : XCTestCase diff --git a/packages/local_auth/local_auth/example/ios/RunnerTests/Info.plist b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/local_auth/local_auth/example/ios/RunnerTests/Info.plist rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/Info.plist diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/MetaDataUtilTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/MetaDataUtilTests.m similarity index 98% rename from packages/image_picker/image_picker/example/ios/RunnerTests/MetaDataUtilTests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/MetaDataUtilTests.m index 4160c51cc600..b684a214570b 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/MetaDataUtilTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/MetaDataUtilTests.m @@ -4,8 +4,8 @@ #import "ImagePickerTestImages.h" -@import image_picker; -@import image_picker.Test; +@import image_picker_ios; +@import image_picker_ios.Test; @import XCTest; @interface MetaDataUtilTests : XCTestCase diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/PhotoAssetUtilTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m similarity index 99% rename from packages/image_picker/image_picker/example/ios/RunnerTests/PhotoAssetUtilTests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m index 97b4b6cd8eb3..d211ea3f91df 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/PhotoAssetUtilTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m @@ -4,8 +4,8 @@ #import "ImagePickerTestImages.h" -@import image_picker; -@import image_picker.Test; +@import image_picker_ios; +@import image_picker_ios.Test; @import XCTest; @interface PhotoAssetUtilTests : XCTestCase diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m similarity index 75% rename from packages/image_picker/image_picker/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m index f94db83d5696..e04c4f2abb50 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m @@ -5,8 +5,8 @@ #import #import -@import image_picker; -@import image_picker.Test; +@import image_picker_ios; +@import image_picker_ios.Test; @import XCTest; @interface PickerSaveImageToPathOperationTests : XCTestCase @@ -22,7 +22,7 @@ - (void)testSaveWebPImage API_AVAILABLE(ios(14)) { PHPickerResult *result = [self createPickerResultWithProvider:itemProvider withIdentifier:UTTypeWebP.identifier]; - [self verifySavingImageWithPickerResult:result]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES]; } - (void)testSavePNGImage API_AVAILABLE(ios(14)) { @@ -32,7 +32,7 @@ - (void)testSavePNGImage API_AVAILABLE(ios(14)) { PHPickerResult *result = [self createPickerResultWithProvider:itemProvider withIdentifier:UTTypeWebP.identifier]; - [self verifySavingImageWithPickerResult:result]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES]; } - (void)testSaveJPGImage API_AVAILABLE(ios(14)) { @@ -42,7 +42,7 @@ - (void)testSaveJPGImage API_AVAILABLE(ios(14)) { PHPickerResult *result = [self createPickerResultWithProvider:itemProvider withIdentifier:UTTypeWebP.identifier]; - [self verifySavingImageWithPickerResult:result]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES]; } - (void)testSaveGIFImage API_AVAILABLE(ios(14)) { @@ -52,7 +52,21 @@ - (void)testSaveGIFImage API_AVAILABLE(ios(14)) { PHPickerResult *result = [self createPickerResultWithProvider:itemProvider withIdentifier:UTTypeWebP.identifier]; - [self verifySavingImageWithPickerResult:result]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES]; +} + +- (void)testSavePNGImageWithoutFullMetadata API_AVAILABLE(ios(14)) { + id photoAssetUtil = OCMClassMock([PHAsset class]); + + NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"pngImage" + withExtension:@"png"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider + withIdentifier:UTTypeWebP.identifier]; + + [self verifySavingImageWithPickerResult:result fullMetadata:NO]; + OCMVerify(times(0), [photoAssetUtil fetchAssetsWithLocalIdentifiers:[OCMArg any] + options:[OCMArg any]]); } /** @@ -79,7 +93,8 @@ - (PHPickerResult *)createPickerResultWithProvider:(NSItemProvider *)itemProvide * * @param result the picker result */ -- (void)verifySavingImageWithPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14)) { +- (void)verifySavingImageWithPickerResult:(PHPickerResult *)result + fullMetadata:(BOOL)fullMetadata API_AVAILABLE(ios(14)) { XCTestExpectation *pathExpectation = [self expectationWithDescription:@"Path was created"]; FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc] @@ -87,6 +102,7 @@ - (void)verifySavingImageWithPickerResult:(PHPickerResult *)result API_AVAILABLE maxHeight:@100 maxWidth:@100 desiredImageQuality:@100 + fullMetadata:fullMetadata savedPathBlock:^(NSString *savedPath) { if ([[NSFileManager defaultManager] fileExistsAtPath:savedPath]) { [pathExpectation fulfill]; diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m similarity index 100% rename from packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m similarity index 100% rename from packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m rename to packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/Info.plist b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/gifImage.gif b/packages/image_picker/image_picker_ios/example/ios/TestImages/gifImage.gif new file mode 100644 index 000000000000..5f989fcf40c7 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/TestImages/gifImage.gif differ diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/jpgImage.jpg b/packages/image_picker/image_picker_ios/example/ios/TestImages/jpgImage.jpg new file mode 100644 index 000000000000..12b2dc17624c Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/TestImages/jpgImage.jpg differ diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/pngImage.png b/packages/image_picker/image_picker_ios/example/ios/TestImages/pngImage.png new file mode 100644 index 000000000000..d7ad7d3968e9 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/TestImages/pngImage.png differ diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/webpImage.webp b/packages/image_picker/image_picker_ios/example/ios/TestImages/webpImage.webp new file mode 100644 index 000000000000..ab7d40d83968 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/TestImages/webpImage.webp differ diff --git a/packages/image_picker/image_picker_ios/example/ios/image_picker_exampleTests/Info.plist b/packages/image_picker/image_picker_ios/example/ios/image_picker_exampleTests/Info.plist new file mode 100644 index 000000000000..6c40a6cd0c4a --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/image_picker_exampleTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/image_picker/image_picker_ios/example/lib/main.dart b/packages/image_picker/image_picker_ios/example/lib/main.dart new file mode 100755 index 000000000000..440f2f1d7cca --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/lib/main.dart @@ -0,0 +1,428 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Image Picker Demo', + home: MyHomePage(title: 'Image Picker Example'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, this.title}) : super(key: key); + + final String? title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + List? _imageFileList; + + void _setImageFileListFromFile(XFile? value) { + _imageFileList = value == null ? null : [value]; + } + + dynamic _pickImageError; + bool isVideo = false; + + VideoPlayerController? _controller; + VideoPlayerController? _toBeDisposed; + String? _retrieveDataError; + + final ImagePickerPlatform _picker = ImagePickerPlatform.instance; + final TextEditingController maxWidthController = TextEditingController(); + final TextEditingController maxHeightController = TextEditingController(); + final TextEditingController qualityController = TextEditingController(); + + Future _playVideo(XFile? file) async { + if (file != null && mounted) { + await _disposeVideoController(); + late VideoPlayerController controller; + if (kIsWeb) { + controller = VideoPlayerController.network(file.path); + } else { + controller = VideoPlayerController.file(File(file.path)); + } + _controller = controller; + // In web, most browsers won't honor a programmatic call to .play + // if the video has a sound track (and is not muted). + // Mute the video so it auto-plays in web! + // This is not needed if the call to .play is the result of user + // interaction (clicking on a "play" button, for example). + const double volume = kIsWeb ? 0.0 : 1.0; + await controller.setVolume(volume); + await controller.initialize(); + await controller.setLooping(true); + await controller.play(); + setState(() {}); + } + } + + Future _onImageButtonPressed(ImageSource source, + {BuildContext? context, 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)); + await _playVideo(file); + } else if (isMultiImage) { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = + await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); + setState(() { + _imageFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + } + + @override + void deactivate() { + if (_controller != null) { + _controller!.setVolume(0.0); + _controller!.pause(); + } + super.deactivate(); + } + + @override + void dispose() { + _disposeVideoController(); + maxWidthController.dispose(); + maxHeightController.dispose(); + qualityController.dispose(); + super.dispose(); + } + + Future _disposeVideoController() async { + if (_toBeDisposed != null) { + await _toBeDisposed!.dispose(); + } + _toBeDisposed = _controller; + _controller = null; + } + + Widget _previewVideo() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_controller == null) { + return const Text( + 'You have not yet picked a video', + textAlign: TextAlign.center, + ); + } + return Padding( + padding: const EdgeInsets.all(10.0), + child: AspectRatioVideo(_controller), + ); + } + + Widget _previewImages() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_imageFileList != null) { + return Semantics( + label: 'image_picker_example_picked_images', + child: ListView.builder( + key: UniqueKey(), + itemBuilder: (BuildContext context, int index) { + // 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)), + ); + }, + itemCount: _imageFileList!.length, + ), + ); + } else if (_pickImageError != null) { + return Text( + 'Pick image error: $_pickImageError', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + + Widget _handlePreview() { + if (isVideo) { + return _previewVideo(); + } else { + return _previewImages(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title!), + ), + body: Center( + child: _handlePreview(), + ), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Semantics( + label: 'image_picker_example_from_gallery', + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'image0', + tooltip: 'Pick Image from gallery', + child: const Icon(Icons.photo), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); + }, + heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + isVideo = true; + _onImageButtonPressed(ImageSource.gallery); + }, + heroTag: 'video0', + tooltip: 'Pick Video from gallery', + child: const Icon(Icons.video_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + isVideo = true; + _onImageButtonPressed(ImageSource.camera); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), + ), + ], + ), + ); + } + + Text? _getRetrieveErrorWidget() { + if (_retrieveDataError != null) { + final Text result = Text(_retrieveDataError!); + _retrieveDataError = null; + return result; + } + return null; + } + + Future _displayPickImageDialog( + BuildContext context, OnPickImageCallback onPick) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Add optional parameters'), + content: Column( + children: [ + TextField( + controller: maxWidthController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxWidth if desired'), + ), + TextField( + controller: maxHeightController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxHeight if desired'), + ), + TextField( + controller: qualityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter quality if desired'), + ), + ], + ), + actions: [ + TextButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('PICK'), + onPressed: () { + final double? width = maxWidthController.text.isNotEmpty + ? double.parse(maxWidthController.text) + : null; + final double? height = maxHeightController.text.isNotEmpty + ? double.parse(maxHeightController.text) + : null; + final int? quality = qualityController.text.isNotEmpty + ? int.parse(qualityController.text) + : null; + onPick(width, height, quality); + Navigator.of(context).pop(); + }), + ], + ); + }); + } +} + +typedef OnPickImageCallback = void Function( + double? maxWidth, double? maxHeight, int? quality); + +class AspectRatioVideo extends StatefulWidget { + const AspectRatioVideo(this.controller, {Key? key}) : super(key: key); + + final VideoPlayerController? controller; + + @override + AspectRatioVideoState createState() => AspectRatioVideoState(); +} + +class AspectRatioVideoState extends State { + VideoPlayerController? get controller => widget.controller; + bool initialized = false; + + void _onVideoControllerUpdate() { + if (!mounted) { + return; + } + if (initialized != controller!.value.isInitialized) { + initialized = controller!.value.isInitialized; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + controller!.addListener(_onVideoControllerUpdate); + } + + @override + void dispose() { + controller!.removeListener(_onVideoControllerUpdate); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (initialized) { + return Center( + child: AspectRatio( + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller!), + ), + ); + } else { + return Container(); + } + } +} diff --git a/packages/image_picker/image_picker_ios/example/pubspec.yaml b/packages/image_picker/image_picker_ios/example/pubspec.yaml new file mode 100755 index 000000000000..856f775cc641 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/pubspec.yaml @@ -0,0 +1,31 @@ +name: image_picker_example +description: Demonstrates how to use the image_picker plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + flutter: + sdk: flutter + image_picker_ios: + # When depending on this package from a real application you should use: + # image_picker_ios: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + image_picker_platform_interface: ^2.6.1 + video_player: ^2.1.4 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/image_picker/image_picker_ios/example/test_driver/integration_test.dart b/packages/image_picker/image_picker_ios/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/test_driver/integration_test.dart @@ -0,0 +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. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/local_auth/local_auth/ios/Assets/.gitkeep b/packages/image_picker/image_picker_ios/ios/Assets/.gitkeep old mode 100644 new mode 100755 similarity index 100% rename from packages/local_auth/local_auth/ios/Assets/.gitkeep rename to packages/image_picker/image_picker_ios/ios/Assets/.gitkeep diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.h similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.h rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.h diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.m rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.h similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.h diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.m similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.m diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m similarity index 98% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m index 4c705fe54350..37a1a9897cd3 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m @@ -14,6 +14,8 @@ + (PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info { if (@available(iOS 11, *)) { return [info objectForKey:UIImagePickerControllerPHAsset]; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" NSURL *referenceURL = [info objectForKey:UIImagePickerControllerReferenceURL]; if (!referenceURL) { return nil; @@ -21,6 +23,7 @@ + (PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info { PHFetchResult *result = [PHAsset fetchAssetsWithALAssetURLs:@[ referenceURL ] options:nil]; return result.firstObject; +#pragma clang diagnostic pop } + (PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14)) { diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h similarity index 100% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m similarity index 54% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m index cc841d6db447..27b06ba994ef 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m @@ -16,31 +16,24 @@ #import "FLTImagePickerMetaDataUtil.h" #import "FLTImagePickerPhotoAssetUtil.h" #import "FLTPHPickerSaveImageToPathOperation.h" +#import "messages.g.h" -/** - * Returns the value for the given key in 'dict', or nil if the value is - * NSNull. - */ -id GetNullableValueForKey(NSDictionary *dict, NSString *key) { - id value = dict[key]; - return value == [NSNull null] ? nil : value; +@implementation FLTImagePickerMethodCallContext +- (instancetype)initWithResult:(nonnull FlutterResultAdapter)result { + if (self = [super init]) { + _result = [result copy]; + } + return self; } +@end + +#pragma mark - @interface FLTImagePickerPlugin () -/** - * The maximum amount of images that are allowed to be picked. - */ -@property(assign, nonatomic) int maxImagesAllowed; - -/** - * The arguments that are passed in from the Flutter method call. - */ -@property(copy, nonatomic) NSDictionary *arguments; - /** * The PHPickerViewController instance used to pick multiple * images. @@ -58,19 +51,13 @@ @interface FLTImagePickerPlugin () *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/image_picker" - binaryMessenger:[registrar messenger]]; - FLTImagePickerPlugin *instance = [FLTImagePickerPlugin new]; - [registrar addMethodCallDelegate:instance channel:channel]; + FLTImagePickerPlugin *instance = [[FLTImagePickerPlugin alloc] init]; + FLTImagePickerApiSetup(registrar.messenger, instance); } - (UIImagePickerController *)createImagePickerController { @@ -107,130 +94,197 @@ - (UIViewController *)viewControllerWithWindow:(UIWindow *)window { } /** - * Returns the UIImagePickerControllerCameraDevice to use given [arguments]. + * Returns the UIImagePickerControllerCameraDevice to use given [source]. * - * If the cameraDevice value that is fetched from arguments is 1 then returns - * UIImagePickerControllerCameraDeviceFront. If the cameraDevice value that is fetched - * from arguments is 0 then returns UIImagePickerControllerCameraDeviceRear. - * - * @param arguments that should be used to get cameraDevice value. + * @param source The source specification from Dart. */ -- (UIImagePickerControllerCameraDevice)getCameraDeviceFromArguments:(NSDictionary *)arguments { - NSInteger cameraDevice = [arguments[@"cameraDevice"] intValue]; - return (cameraDevice == 1) ? UIImagePickerControllerCameraDeviceFront - : UIImagePickerControllerCameraDeviceRear; +- (UIImagePickerControllerCameraDevice)cameraDeviceForSource:(FLTSourceSpecification *)source { + switch (source.camera) { + case FLTSourceCameraFront: + return UIImagePickerControllerCameraDeviceFront; + case FLTSourceCameraRear: + return UIImagePickerControllerCameraDeviceRear; + } } -- (void)pickImageWithPHPicker:(int)maxImagesAllowed API_AVAILABLE(ios(14)) { +- (void)launchPHPickerWithContext:(nonnull FLTImagePickerMethodCallContext *)context + API_AVAILABLE(ios(14)) { PHPickerConfiguration *config = [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; - config.selectionLimit = maxImagesAllowed; // Setting to zero allow us to pick unlimited photos + config.selectionLimit = context.maxImageCount; config.filter = [PHPickerFilter imagesFilter]; _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; _pickerViewController.delegate = self; _pickerViewController.presentationController.delegate = self; + self.callContext = context; - self.maxImagesAllowed = maxImagesAllowed; - - [self checkPhotoAuthorizationForAccessLevel]; + if (context.requestFullMetadata) { + [self checkPhotoAuthorizationForAccessLevel]; + } else { + [self showPhotoLibraryWithPHPicker:_pickerViewController]; + } } -- (void)launchUIImagePickerWithSource:(int)imageSource { +- (void)launchUIImagePickerWithSource:(nonnull FLTSourceSpecification *)source + context:(nonnull FLTImagePickerMethodCallContext *)context { UIImagePickerController *imagePickerController = [self createImagePickerController]; imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; imagePickerController.delegate = self; imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; + self.callContext = context; - self.maxImagesAllowed = 1; - - switch (imageSource) { - case SOURCE_CAMERA: - [self checkCameraAuthorizationWithImagePicker:imagePickerController]; + switch (source.type) { + case FLTSourceTypeCamera: + [self checkCameraAuthorizationWithImagePicker:imagePickerController + camera:[self cameraDeviceForSource:source]]; break; - case SOURCE_GALLERY: - [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; + case FLTSourceTypeGallery: + if (@available(iOS 11, *)) { + if (context.requestFullMetadata) { + [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; + } else { + [self showPhotoLibraryWithImagePicker:imagePickerController]; + } + } else { + // Prior to iOS 11, accessing gallery requires authorization + [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; + } break; default: - self.result([FlutterError errorWithCode:@"invalid_source" - message:@"Invalid image source." - details:nil]); + [self sendCallResultWithError:[FlutterError errorWithCode:@"invalid_source" + message:@"Invalid image source." + details:nil]]; break; } } -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if (self.result) { - self.result([FlutterError errorWithCode:@"multiple_request" - message:@"Cancelled by a second request" - details:nil]); - self.result = nil; - } - - self.result = result; - _arguments = call.arguments; - - if ([@"pickImage" isEqualToString:call.method]) { - int imageSource = [call.arguments[@"source"] intValue]; +#pragma mark - FLTImagePickerApi + +- (void)pickImageWithSource:(nonnull FLTSourceSpecification *)source + maxSize:(nonnull FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + fullMetadata:(NSNumber *)fullMetadata + completion: + (nonnull void (^)(NSString *_Nullable, FlutterError *_Nullable))completion { + [self cancelInProgressCall]; + FLTImagePickerMethodCallContext *context = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^void(NSArray *paths, FlutterError *error) { + if (paths && paths.count != 1) { + completion(nil, [FlutterError errorWithCode:@"invalid_result" + message:@"Incorrect number of return paths provided" + details:nil]); + } + completion(paths.firstObject, error); + }]; + context.maxSize = maxSize; + context.imageQuality = imageQuality; + context.maxImageCount = 1; + context.requestFullMetadata = [fullMetadata boolValue]; - if (imageSource == SOURCE_GALLERY) { // Capture is not possible with PHPicker - if (@available(iOS 14, *)) { - // PHPicker is used - [self pickImageWithPHPicker:1]; - } else { - // UIImagePicker is used - [self launchUIImagePickerWithSource:imageSource]; - } - } else { - [self launchUIImagePickerWithSource:imageSource]; - } - } else if ([@"pickMultiImage" isEqualToString:call.method]) { + if (source.type == FLTSourceTypeGallery) { // Capture is not possible with PHPicker if (@available(iOS 14, *)) { - [self pickImageWithPHPicker:0]; + [self launchPHPickerWithContext:context]; } else { - [self launchUIImagePickerWithSource:SOURCE_GALLERY]; - } - } else if ([@"pickVideo" isEqualToString:call.method]) { - UIImagePickerController *imagePickerController = [self createImagePickerController]; - imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; - imagePickerController.delegate = self; - imagePickerController.mediaTypes = @[ - (NSString *)kUTTypeMovie, (NSString *)kUTTypeAVIMovie, (NSString *)kUTTypeVideo, - (NSString *)kUTTypeMPEG4 - ]; - imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; - - int imageSource = [call.arguments[@"source"] intValue]; - if ([call.arguments[@"maxDuration"] isKindOfClass:[NSNumber class]]) { - NSTimeInterval max = [call.arguments[@"maxDuration"] doubleValue]; - imagePickerController.videoMaximumDuration = max; + [self launchUIImagePickerWithSource:source context:context]; } + } else { + [self launchUIImagePickerWithSource:source context:context]; + } +} - switch (imageSource) { - case SOURCE_CAMERA: - [self checkCameraAuthorizationWithImagePicker:imagePickerController]; - break; - case SOURCE_GALLERY: - [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; - break; - default: - result([FlutterError errorWithCode:@"invalid_source" - message:@"Invalid video source." - details:nil]); - break; - } +- (void)pickMultiImageWithMaxSize:(nonnull FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + fullMetadata:(NSNumber *)fullMetadata + completion:(nonnull void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion { + FLTImagePickerMethodCallContext *context = + [[FLTImagePickerMethodCallContext alloc] initWithResult:completion]; + context.maxSize = maxSize; + context.imageQuality = imageQuality; + context.requestFullMetadata = [fullMetadata boolValue]; + + if (@available(iOS 14, *)) { + [self launchPHPickerWithContext:context]; } else { - result(FlutterMethodNotImplemented); + // Camera is ignored for gallery mode, so the value here is arbitrary. + [self launchUIImagePickerWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery + camera:FLTSourceCameraRear] + context:context]; } } -- (void)showCameraWithImagePicker:(UIImagePickerController *)imagePickerController { +- (void)pickVideoWithSource:(nonnull FLTSourceSpecification *)source + maxDuration:(nullable NSNumber *)maxDurationSeconds + completion: + (nonnull void (^)(NSString *_Nullable, FlutterError *_Nullable))completion { + FLTImagePickerMethodCallContext *context = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^void(NSArray *paths, FlutterError *error) { + if (paths && paths.count != 1) { + completion(nil, [FlutterError errorWithCode:@"invalid_result" + message:@"Incorrect number of return paths provided" + details:nil]); + } + completion(paths.firstObject, error); + }]; + context.maxImageCount = 1; + + UIImagePickerController *imagePickerController = [self createImagePickerController]; + imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; + imagePickerController.delegate = self; + imagePickerController.mediaTypes = @[ + (NSString *)kUTTypeMovie, (NSString *)kUTTypeAVIMovie, (NSString *)kUTTypeVideo, + (NSString *)kUTTypeMPEG4 + ]; + imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; + + if (maxDurationSeconds) { + NSTimeInterval max = [maxDurationSeconds doubleValue]; + imagePickerController.videoMaximumDuration = max; + } + + self.callContext = context; + + switch (source.type) { + case FLTSourceTypeCamera: + [self checkCameraAuthorizationWithImagePicker:imagePickerController + camera:[self cameraDeviceForSource:source]]; + break; + case FLTSourceTypeGallery: + [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; + break; + default: + [self sendCallResultWithError:[FlutterError errorWithCode:@"invalid_source" + message:@"Invalid video source." + details:nil]]; + break; + } +} + +#pragma mark - + +/** + * If a call is still in progress, cancels it by returning an error and then clearing state. + * + * TODO(stuartmorgan): Eliminate this, and instead track context per image picker (e.g., using + * associated objects). + */ +- (void)cancelInProgressCall { + if (self.callContext) { + [self sendCallResultWithError:[FlutterError errorWithCode:@"multiple_request" + message:@"Cancelled by a second request" + details:nil]]; + self.callContext = nil; + } +} + +- (void)showCamera:(UIImagePickerControllerCameraDevice)device + withImagePicker:(UIImagePickerController *)imagePickerController { @synchronized(self) { if (imagePickerController.beingPresented) { return; } } - UIImagePickerControllerCameraDevice device = [self getCameraDeviceFromArguments:_arguments]; // Camera is not available on simulators if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] && [UIImagePickerController isCameraDeviceAvailable:device]) { @@ -254,25 +308,24 @@ - (void)showCameraWithImagePicker:(UIImagePickerController *)imagePickerControll [[self viewControllerWithWindow:nil] presentViewController:cameraErrorAlert animated:YES completion:nil]; - self.result(nil); - self.result = nil; - _arguments = nil; + [self sendCallResultWithSavedPathList:nil]; } } -- (void)checkCameraAuthorizationWithImagePicker:(UIImagePickerController *)imagePickerController { +- (void)checkCameraAuthorizationWithImagePicker:(UIImagePickerController *)imagePickerController + camera:(UIImagePickerControllerCameraDevice)device { AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; switch (status) { case AVAuthorizationStatusAuthorized: - [self showCameraWithImagePicker:imagePickerController]; + [self showCamera:device withImagePicker:imagePickerController]; break; case AVAuthorizationStatusNotDetermined: { [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { dispatch_async(dispatch_get_main_queue(), ^{ if (granted) { - [self showCameraWithImagePicker:imagePickerController]; + [self showCamera:device withImagePicker:imagePickerController]; } else { [self errorNoCameraAccess:AVAuthorizationStatusDenied]; } @@ -352,15 +405,17 @@ - (void)checkPhotoAuthorizationForAccessLevel API_AVAILABLE(ios(14)) { - (void)errorNoCameraAccess:(AVAuthorizationStatus)status { switch (status) { case AVAuthorizationStatusRestricted: - self.result([FlutterError errorWithCode:@"camera_access_restricted" - message:@"The user is not allowed to use the camera." - details:nil]); + [self sendCallResultWithError:[FlutterError + errorWithCode:@"camera_access_restricted" + message:@"The user is not allowed to use the camera." + details:nil]]; break; case AVAuthorizationStatusDenied: default: - self.result([FlutterError errorWithCode:@"camera_access_denied" - message:@"The user did not allow camera access." - details:nil]); + [self sendCallResultWithError:[FlutterError + errorWithCode:@"camera_access_denied" + message:@"The user did not allow camera access." + details:nil]]; break; } } @@ -368,15 +423,17 @@ - (void)errorNoCameraAccess:(AVAuthorizationStatus)status { - (void)errorNoPhotoAccess:(PHAuthorizationStatus)status { switch (status) { case PHAuthorizationStatusRestricted: - self.result([FlutterError errorWithCode:@"photo_access_restricted" - message:@"The user is not allowed to use the photo." - details:nil]); + [self sendCallResultWithError:[FlutterError + errorWithCode:@"photo_access_restricted" + message:@"The user is not allowed to use the photo." + details:nil]]; break; case PHAuthorizationStatusDenied: default: - self.result([FlutterError errorWithCode:@"photo_access_denied" - message:@"The user did not allow photo access." - details:nil]); + [self sendCallResultWithError:[FlutterError + errorWithCode:@"photo_access_denied" + message:@"The user did not allow photo access." + details:nil]]; break; } } @@ -406,54 +463,53 @@ - (NSNumber *)getDesiredImageQuality:(NSNumber *)imageQuality { return imageQuality; } +#pragma mark - UIAdaptivePresentationControllerDelegate + - (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController { - if (self.result != nil) { - self.result(nil); - self.result = nil; - self->_arguments = nil; - } + [self sendCallResultWithSavedPathList:nil]; } +#pragma mark - PHPickerViewControllerDelegate + - (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray *)results API_AVAILABLE(ios(14)) { [picker dismissViewControllerAnimated:YES completion:nil]; if (results.count == 0) { - if (self.result != nil) { - self.result(nil); - self.result = nil; - self->_arguments = nil; - } + [self sendCallResultWithSavedPathList:nil]; return; } dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); dispatch_async(backgroundQueue, ^{ - NSNumber *maxWidth = GetNullableValueForKey(self->_arguments, @"maxWidth"); - NSNumber *maxHeight = GetNullableValueForKey(self->_arguments, @"maxHeight"); - NSNumber *imageQuality = GetNullableValueForKey(self->_arguments, @"imageQuality"); + NSNumber *maxWidth = self.callContext.maxSize.width; + NSNumber *maxHeight = self.callContext.maxSize.height; + NSNumber *imageQuality = self.callContext.imageQuality; NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; NSOperationQueue *operationQueue = [NSOperationQueue new]; NSMutableArray *pathList = [self createNSMutableArrayWithSize:results.count]; for (int i = 0; i < results.count; i++) { PHPickerResult *result = results[i]; - FLTPHPickerSaveImageToPathOperation *operation = - [[FLTPHPickerSaveImageToPathOperation alloc] initWithResult:result - maxHeight:maxHeight - maxWidth:maxWidth - desiredImageQuality:desiredImageQuality - savedPathBlock:^(NSString *savedPath) { - pathList[i] = savedPath; - }]; + FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc] + initWithResult:result + maxHeight:maxHeight + maxWidth:maxWidth + desiredImageQuality:desiredImageQuality + fullMetadata:self.callContext.requestFullMetadata + savedPathBlock:^(NSString *savedPath) { + pathList[i] = savedPath; + }]; [operationQueue addOperation:operation]; } [operationQueue waitUntilAllOperationsAreFinished]; dispatch_async(dispatch_get_main_queue(), ^{ - [self handleSavedPathList:pathList]; + [self sendCallResultWithSavedPathList:pathList]; }); }); } +#pragma mark - + /** * Creates an NSMutableArray of a certain size filled with NSNull objects. * @@ -470,6 +526,8 @@ - (NSMutableArray *)createNSMutableArrayWithSize:(NSUInteger)size { return mutableArray; } +#pragma mark - UIImagePickerControllerDelegate + - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { NSURL *videoURL = info[UIImagePickerControllerMediaURL]; @@ -478,7 +536,7 @@ - (void)imagePickerController:(UIImagePickerController *)picker // further didFinishPickingMediaWithInfo invocations. A nil check is necessary // to prevent below code to be unwantly executed multiple times and cause a // crash. - if (!self.result) { + if (!self.callContext) { return; } if (videoURL != nil) { @@ -493,30 +551,32 @@ - (void)imagePickerController:(UIImagePickerController *)picker [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error]; if (error) { - self.result([FlutterError errorWithCode:@"flutter_image_picker_copy_video_error" - message:@"Could not cache the video file." - details:nil]); - self.result = nil; + [self sendCallResultWithError:[FlutterError + errorWithCode:@"flutter_image_picker_copy_video_error" + message:@"Could not cache the video file." + details:nil]]; return; } } videoURL = destination; } } - self.result(videoURL.path); - self.result = nil; - _arguments = nil; + [self sendCallResultWithSavedPathList:@[ videoURL.path ]]; } else { UIImage *image = info[UIImagePickerControllerEditedImage]; if (image == nil) { image = info[UIImagePickerControllerOriginalImage]; } - NSNumber *maxWidth = GetNullableValueForKey(_arguments, @"maxWidth"); - NSNumber *maxHeight = GetNullableValueForKey(_arguments, @"maxHeight"); - NSNumber *imageQuality = GetNullableValueForKey(_arguments, @"imageQuality"); + NSNumber *maxWidth = self.callContext.maxSize.width; + NSNumber *maxHeight = self.callContext.maxSize.height; + NSNumber *imageQuality = self.callContext.imageQuality; NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; - PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:info]; + PHAsset *originalAsset; + if (_callContext.requestFullMetadata) { + // Full metadata are available only in PHAsset, which requires gallery permission. + originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:info]; + } if (maxWidth != nil || maxHeight != nil) { image = [FLTImagePickerImageUtil scaledImage:image @@ -529,32 +589,49 @@ - (void)imagePickerController:(UIImagePickerController *)picker // Image picked without an original asset (e.g. User took a photo directly) [self saveImageWithPickerInfo:info image:image imageQuality:desiredImageQuality]; } else { - [[PHImageManager defaultManager] - requestImageDataForAsset:originalAsset - options:nil - resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, - UIImageOrientation orientation, NSDictionary *_Nullable info) { - // maxWidth and maxHeight are used only for GIF images. - [self saveImageWithOriginalImageData:imageData - image:image - maxWidth:maxWidth - maxHeight:maxHeight - imageQuality:desiredImageQuality]; - }]; + void (^resultHandler)(NSData *imageData, NSString *dataUTI, NSDictionary *info) = ^( + NSData *_Nullable imageData, NSString *_Nullable dataUTI, NSDictionary *_Nullable info) { + // maxWidth and maxHeight are used only for GIF images. + [self saveImageWithOriginalImageData:imageData + image:image + maxWidth:maxWidth + maxHeight:maxHeight + imageQuality:desiredImageQuality]; + }; + if (@available(iOS 13.0, *)) { + [[PHImageManager defaultManager] + requestImageDataAndOrientationForAsset:originalAsset + options:nil + resultHandler:^(NSData *_Nullable imageData, + NSString *_Nullable dataUTI, + CGImagePropertyOrientation orientation, + NSDictionary *_Nullable info) { + resultHandler(imageData, dataUTI, info); + }]; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [[PHImageManager defaultManager] + requestImageDataForAsset:originalAsset + options:nil + resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, + UIImageOrientation orientation, + NSDictionary *_Nullable info) { + resultHandler(imageData, dataUTI, info); + }]; +#pragma clang diagnostic pop + } } } } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [picker dismissViewControllerAnimated:YES completion:nil]; - if (!self.result) { - return; - } - self.result(nil); - self.result = nil; - _arguments = nil; + [self sendCallResultWithSavedPathList:nil]; } +#pragma mark - + - (void)saveImageWithOriginalImageData:(NSData *)originalImageData image:(UIImage *)image maxWidth:(NSNumber *)maxWidth @@ -566,7 +643,7 @@ - (void)saveImageWithOriginalImageData:(NSData *)originalImageData maxWidth:maxWidth maxHeight:maxHeight imageQuality:imageQuality]; - [self handleSavedPathList:@[ savedPath ]]; + [self sendCallResultWithSavedPathList:@[ savedPath ]]; } - (void)saveImageWithPickerInfo:(NSDictionary *)info @@ -575,47 +652,36 @@ - (void)saveImageWithPickerInfo:(NSDictionary *)info NSString *savedPath = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:info image:image imageQuality:imageQuality]; - [self handleSavedPathList:@[ savedPath ]]; + [self sendCallResultWithSavedPathList:@[ savedPath ]]; } -/** - * Applies NSMutableArray on the FLutterResult. - * - * NSString must be returned by FlutterResult if the single image - * mode is active. It is checked by maxImagesAllowed and - * returns the first object of the pathlist. - * - * NSMutableArray must be returned by FlutterResult if the multi-image - * mode is active. After the pathlist count is checked then it returns - * the pathlist. - * - * @param pathList that should be applied to FlutterResult. - */ -- (void)handleSavedPathList:(NSArray *)pathList { - if (!self.result) { +- (void)sendCallResultWithSavedPathList:(nullable NSArray *)pathList { + if (!self.callContext) { return; } - if (pathList) { - if (![pathList containsObject:[NSNull null]]) { - if ((self.maxImagesAllowed == 1)) { - self.result(pathList.firstObject); - } else { - self.result(pathList); - } - } else { - self.result([FlutterError errorWithCode:@"create_error" - message:@"pathList's items should not be null" - details:nil]); - } + if ([pathList containsObject:[NSNull null]]) { + self.callContext.result(nil, [FlutterError errorWithCode:@"create_error" + message:@"pathList's items should not be null" + details:nil]); } else { - // This should never happen. - self.result([FlutterError errorWithCode:@"create_error" - message:@"pathList should not be nil" - details:nil]); + self.callContext.result(pathList, nil); + } + self.callContext = nil; +} + +/** + * Sends the given error via `callContext.result` as the result of the original platform channel + * method call, clearing the in-progress call state. + * + * @param error The error to return. + */ +- (void)sendCallResultWithError:(FlutterError *)error { + if (!self.callContext) { + return; } - self.result = nil; - _arguments = nil; + self.callContext.result(nil, error); + self.callContext = nil; } @end diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h new file mode 100644 index 000000000000..d73a54d245f6 --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h @@ -0,0 +1,98 @@ +// 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 header is available in the Test module. Import via "@import image_picker_ios_ios.Test;" + +#import + +#import "messages.g.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The return hander used for all method calls, which internally adapts the provided result list + * to return either a list or a single element depending on the original call. + */ +typedef void (^FlutterResultAdapter)(NSArray *_Nullable, FlutterError *_Nullable); + +/** + * A container class for context to use when handling a method call from the Dart side. + */ +@interface FLTImagePickerMethodCallContext : NSObject + +/** + * Initializes a new context that calls |result| on completion of the operation. + */ +- (instancetype)initWithResult:(nonnull FlutterResultAdapter)result; + +/** The callback to provide results to the Dart caller. */ +@property(nonatomic, copy, nonnull) FlutterResultAdapter result; + +/** + * The maximum size to enforce on the results. + * + * If nil, no resizing is done. + */ +@property(nonatomic, strong, nullable) FLTMaxSize *maxSize; + +/** + * The image quality to resample the results to. + * + * If nil, no resampling is done. + */ +@property(nonatomic, strong, nullable) NSNumber *imageQuality; + +/** Maximum number of images to select. 0 indicates no maximum. */ +@property(nonatomic, assign) int maxImageCount; + +/** Whether the image should be picked with full metadata (requires gallery permissions) */ +@property(nonatomic, assign) BOOL requestFullMetadata; + +@end + +#pragma mark - + +/** Methods exposed for unit testing. */ +@interface FLTImagePickerPlugin () + +/** + * The context of the Flutter method call that is currently being handled, if any. + */ +@property(strong, nonatomic, nullable) FLTImagePickerMethodCallContext *callContext; + +/** + * Validates the provided paths list, then sends it via `callContext.result` as the result of the + * original platform channel method call, clearing the in-progress call state. + * + * @param pathList The paths to return. nil indicates a cancelled operation. + */ +- (void)sendCallResultWithSavedPathList:(nullable NSArray *)pathList; + +/** + * Tells the delegate that the user cancelled the pick operation. + * + * Your delegate’s implementation of this method should dismiss the picker view + * by calling the dismissModalViewControllerAnimated: method of the parent + * view controller. + * + * Implementation of this method is optional, but expected. + * + * @param picker The controller object managing the image picker interface. + */ +- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker; + +/** + * Sets UIImagePickerController instances that will be used when a new + * controller would normally be created. Each call to + * createImagePickerController will remove the current first element from + * the array. + * + * Should be used for testing purposes only. + */ +- (void)setImagePickerControllerOverrides: + (NSArray *)imagePickerControllers; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h similarity index 95% rename from packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h rename to packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h index 7ba3d28ef3dd..8e0970725e90 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h @@ -26,6 +26,7 @@ maxHeight:(NSNumber *)maxHeight maxWidth:(NSNumber *)maxWidth desiredImageQuality:(NSNumber *)desiredImageQuality + fullMetadata:(BOOL)fullMetadata savedPathBlock:(void (^)(NSString *))savedPathBlock API_AVAILABLE(ios(14)); @end diff --git a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m similarity index 63% rename from packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m rename to packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m index 9e7e7e87a30c..7c8fbc9ca7cf 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m @@ -13,6 +13,7 @@ @interface FLTPHPickerSaveImageToPathOperation () @property(assign, nonatomic) NSNumber *maxHeight; @property(assign, nonatomic) NSNumber *maxWidth; @property(assign, nonatomic) NSNumber *desiredImageQuality; +@property(assign, nonatomic) BOOL requestFullMetadata; @end @@ -28,6 +29,7 @@ - (instancetype)initWithResult:(PHPickerResult *)result maxHeight:(NSNumber *)maxHeight maxWidth:(NSNumber *)maxWidth desiredImageQuality:(NSNumber *)desiredImageQuality + fullMetadata:(BOOL)fullMetadata savedPathBlock:(GetSavedPath)savedPathBlock API_AVAILABLE(ios(14)) { if (self = [super init]) { if (result) { @@ -35,6 +37,7 @@ - (instancetype)initWithResult:(PHPickerResult *)result self.maxHeight = maxHeight; self.maxWidth = maxWidth; self.desiredImageQuality = desiredImageQuality; + self.requestFullMetadata = fullMetadata; getSavedPath = savedPathBlock; executing = NO; finished = NO; @@ -113,7 +116,12 @@ - (void)start { * Processes the image. */ - (void)processImage:(UIImage *)localImage API_AVAILABLE(ios(14)) { - PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:self.result]; + PHAsset *originalAsset; + // Only if requested, fetch the full "PHAsset" metadata, which requires "Photo Library Usage" + // permissions. + if (self.requestFullMetadata) { + originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:self.result]; + } if (self.maxWidth != nil || self.maxHeight != nil) { localImage = [FLTImagePickerImageUtil scaledImage:localImage @@ -122,20 +130,39 @@ - (void)processImage:(UIImage *)localImage API_AVAILABLE(ios(14)) { isMetadataAvailable:originalAsset != nil]; } if (originalAsset) { - [[PHImageManager defaultManager] - requestImageDataForAsset:originalAsset - options:nil - resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, - UIImageOrientation orientation, NSDictionary *_Nullable info) { - // maxWidth and maxHeight are used only for GIF images. - NSString *savedPath = [FLTImagePickerPhotoAssetUtil - saveImageWithOriginalImageData:imageData - image:localImage - maxWidth:self.maxWidth - maxHeight:self.maxHeight - imageQuality:self.desiredImageQuality]; - [self completeOperationWithPath:savedPath]; - }]; + void (^resultHandler)(NSData *imageData, NSString *dataUTI, NSDictionary *info) = + ^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, NSDictionary *_Nullable info) { + // maxWidth and maxHeight are used only for GIF images. + NSString *savedPath = [FLTImagePickerPhotoAssetUtil + saveImageWithOriginalImageData:imageData + image:localImage + maxWidth:self.maxWidth + maxHeight:self.maxHeight + imageQuality:self.desiredImageQuality]; + [self completeOperationWithPath:savedPath]; + }; + if (@available(iOS 13.0, *)) { + [[PHImageManager defaultManager] + requestImageDataAndOrientationForAsset:originalAsset + options:nil + resultHandler:^(NSData *_Nullable imageData, + NSString *_Nullable dataUTI, + CGImagePropertyOrientation orientation, + NSDictionary *_Nullable info) { + resultHandler(imageData, dataUTI, info); + }]; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [[PHImageManager defaultManager] + requestImageDataForAsset:originalAsset + options:nil + resultHandler:^(NSData *_Nullable imageData, NSString *_Nullable dataUTI, + UIImageOrientation orientation, NSDictionary *_Nullable info) { + resultHandler(imageData, dataUTI, info); + }]; +#pragma clang diagnostic pop + } } else { // Image picked without an original asset (e.g. User pick image without permission) NSString *savedPath = diff --git a/packages/image_picker/image_picker/ios/Classes/ImagePickerPlugin.modulemap b/packages/image_picker/image_picker_ios/ios/Classes/ImagePickerPlugin.modulemap similarity index 76% rename from packages/image_picker/image_picker/ios/Classes/ImagePickerPlugin.modulemap rename to packages/image_picker/image_picker_ios/ios/Classes/ImagePickerPlugin.modulemap index dc702ea49fb1..0d60b684a256 100644 --- a/packages/image_picker/image_picker/ios/Classes/ImagePickerPlugin.modulemap +++ b/packages/image_picker/image_picker_ios/ios/Classes/ImagePickerPlugin.modulemap @@ -1,6 +1,6 @@ -framework module image_picker { - umbrella header "image_picker-umbrella.h" - +framework module image_picker_ios { + umbrella header "image_picker_ios-umbrella.h" + export * module * { export * } diff --git a/packages/image_picker/image_picker/ios/Classes/image_picker-umbrella.h b/packages/image_picker/image_picker_ios/ios/Classes/image_picker_ios-umbrella.h similarity index 79% rename from packages/image_picker/image_picker/ios/Classes/image_picker-umbrella.h rename to packages/image_picker/image_picker_ios/ios/Classes/image_picker_ios-umbrella.h index 0d89b2e1f636..0e23d6d9d60a 100644 --- a/packages/image_picker/image_picker/ios/Classes/image_picker-umbrella.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/image_picker_ios-umbrella.h @@ -3,4 +3,4 @@ // found in the LICENSE file. #import -#import +#import diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h new file mode 100644 index 000000000000..c87bda59d8fb --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h @@ -0,0 +1,63 @@ +// 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.0.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, FLTSourceCamera) { + FLTSourceCameraRear = 0, + FLTSourceCameraFront = 1, +}; + +typedef NS_ENUM(NSUInteger, FLTSourceType) { + FLTSourceTypeCamera = 0, + FLTSourceTypeGallery = 1, +}; + +@class FLTMaxSize; +@class FLTSourceSpecification; + +@interface FLTMaxSize : NSObject ++ (instancetype)makeWithWidth:(nullable NSNumber *)width height:(nullable NSNumber *)height; +@property(nonatomic, strong, nullable) NSNumber *width; +@property(nonatomic, strong, nullable) NSNumber *height; +@end + +@interface FLTSourceSpecification : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithType:(FLTSourceType)type camera:(FLTSourceCamera)camera; +@property(nonatomic, assign) FLTSourceType type; +@property(nonatomic, assign) FLTSourceCamera camera; +@end + +/// The codec used by FLTImagePickerApi. +NSObject *FLTImagePickerApiGetCodec(void); + +@protocol FLTImagePickerApi +- (void)pickImageWithSource:(FLTSourceSpecification *)source + maxSize:(FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + fullMetadata:(NSNumber *)requestFullMetadata + completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion; +- (void)pickMultiImageWithMaxSize:(FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + fullMetadata:(NSNumber *)requestFullMetadata + completion:(void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion; +- (void)pickVideoWithSource:(FLTSourceSpecification *)source + maxDuration:(nullable NSNumber *)maxDurationSeconds + completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion; +@end + +extern void FLTImagePickerApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +NS_ASSUME_NONNULL_END diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m new file mode 100644 index 000000000000..71a5b5140417 --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m @@ -0,0 +1,222 @@ +// 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.0.3), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import "messages.g.h" +#import + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSDictionary *wrapResult(id result, FlutterError *error) { + NSDictionary *errorDict = (NSDictionary *)[NSNull null]; + if (error) { + errorDict = @{ + @"code" : (error.code ? error.code : [NSNull null]), + @"message" : (error.message ? error.message : [NSNull null]), + @"details" : (error.details ? error.details : [NSNull null]), + }; + } + return @{ + @"result" : (result ? result : [NSNull null]), + @"error" : errorDict, + }; +} +static id GetNullableObject(NSDictionary *dict, id key) { + id result = dict[key]; + return (result == [NSNull null]) ? nil : result; +} +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + +@interface FLTMaxSize () ++ (FLTMaxSize *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FLTSourceSpecification () ++ (FLTSourceSpecification *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end + +@implementation FLTMaxSize ++ (instancetype)makeWithWidth:(nullable NSNumber *)width height:(nullable NSNumber *)height { + FLTMaxSize *pigeonResult = [[FLTMaxSize alloc] init]; + pigeonResult.width = width; + pigeonResult.height = height; + return pigeonResult; +} ++ (FLTMaxSize *)fromMap:(NSDictionary *)dict { + FLTMaxSize *pigeonResult = [[FLTMaxSize alloc] init]; + pigeonResult.width = GetNullableObject(dict, @"width"); + pigeonResult.height = GetNullableObject(dict, @"height"); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.width ? self.width : [NSNull null]), @"width", + (self.height ? self.height : [NSNull null]), @"height", nil]; +} +@end + +@implementation FLTSourceSpecification ++ (instancetype)makeWithType:(FLTSourceType)type camera:(FLTSourceCamera)camera { + FLTSourceSpecification *pigeonResult = [[FLTSourceSpecification alloc] init]; + pigeonResult.type = type; + pigeonResult.camera = camera; + return pigeonResult; +} ++ (FLTSourceSpecification *)fromMap:(NSDictionary *)dict { + FLTSourceSpecification *pigeonResult = [[FLTSourceSpecification alloc] init]; + pigeonResult.type = [GetNullableObject(dict, @"type") integerValue]; + pigeonResult.camera = [GetNullableObject(dict, @"camera") integerValue]; + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:@(self.type), @"type", @(self.camera), @"camera", nil]; +} +@end + +@interface FLTImagePickerApiCodecReader : FlutterStandardReader +@end +@implementation FLTImagePickerApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FLTMaxSize fromMap:[self readValue]]; + + case 129: + return [FLTSourceSpecification fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FLTImagePickerApiCodecWriter : FlutterStandardWriter +@end +@implementation FLTImagePickerApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FLTMaxSize class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FLTSourceSpecification class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FLTImagePickerApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FLTImagePickerApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FLTImagePickerApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FLTImagePickerApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FLTImagePickerApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FLTImagePickerApiCodecReaderWriter *readerWriter = + [[FLTImagePickerApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FLTImagePickerApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickImage" + binaryMessenger:binaryMessenger + codec:FLTImagePickerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (pickImageWithSource:maxSize:quality:fullMetadata:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickImageWithSource:maxSize:quality:fullMetadata:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTSourceSpecification *arg_source = GetNullableObjectAtIndex(args, 0); + FLTMaxSize *arg_maxSize = GetNullableObjectAtIndex(args, 1); + NSNumber *arg_imageQuality = GetNullableObjectAtIndex(args, 2); + NSNumber *arg_requestFullMetadata = GetNullableObjectAtIndex(args, 3); + [api pickImageWithSource:arg_source + maxSize:arg_maxSize + quality:arg_imageQuality + fullMetadata:arg_requestFullMetadata + completion:^(NSString *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickMultiImage" + binaryMessenger:binaryMessenger + codec:FLTImagePickerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (pickMultiImageWithMaxSize:quality:fullMetadata:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickMultiImageWithMaxSize:quality:fullMetadata:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTMaxSize *arg_maxSize = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_imageQuality = GetNullableObjectAtIndex(args, 1); + NSNumber *arg_requestFullMetadata = GetNullableObjectAtIndex(args, 2); + [api pickMultiImageWithMaxSize:arg_maxSize + quality:arg_imageQuality + fullMetadata:arg_requestFullMetadata + completion:^(NSArray *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickVideo" + binaryMessenger:binaryMessenger + codec:FLTImagePickerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pickVideoWithSource:maxDuration:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickVideoWithSource:maxDuration:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTSourceSpecification *arg_source = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_maxDurationSeconds = GetNullableObjectAtIndex(args, 1); + [api pickVideoWithSource:arg_source + maxDuration:arg_maxDurationSeconds + completion:^(NSString *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/image_picker/image_picker/ios/image_picker.podspec b/packages/image_picker/image_picker_ios/ios/image_picker_ios.podspec similarity index 86% rename from packages/image_picker/image_picker/ios/image_picker.podspec rename to packages/image_picker/image_picker_ios/ios/image_picker_ios.podspec index 2a10b1ce01a8..549c5f09e1f8 100644 --- a/packages/image_picker/image_picker/ios/image_picker.podspec +++ b/packages/image_picker/image_picker_ios/ios/image_picker_ios.podspec @@ -2,7 +2,7 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| - s.name = 'image_picker' + s.name = 'image_picker_ios' s.version = '0.0.1' s.summary = 'Flutter plugin that shows an image picker.' s.description = <<-DESC @@ -12,8 +12,8 @@ Downloaded by pub (not CocoaPods). s.homepage = 'https://github.com/flutter/plugins' s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/image_picker' } - s.documentation_url = 'https://pub.dev/packages/image_picker' + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/image_picker_ios' } + s.documentation_url = 'https://pub.dev/packages/image_picker_ios' s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' s.module_map = 'Classes/ImagePickerPlugin.modulemap' 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 new file mode 100644 index 000000000000..fbc356f212b8 --- /dev/null +++ b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart @@ -0,0 +1,249 @@ +// 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:image_picker_platform_interface/image_picker_platform_interface.dart'; + +import 'src/messages.g.dart'; + +// Converts an [ImageSource] to the corresponding Pigeon API enum value. +SourceType _convertSource(ImageSource source) { + switch (source) { + case ImageSource.camera: + return SourceType.camera; + case ImageSource.gallery: + return SourceType.gallery; + default: + throw UnimplementedError('Unknown source: $source'); + } +} + +// Converts a [CameraDevice] to the corresponding Pigeon API enum value. +SourceCamera _convertCamera(CameraDevice camera) { + switch (camera) { + case CameraDevice.front: + return SourceCamera.front; + case CameraDevice.rear: + return SourceCamera.rear; + default: + throw UnimplementedError('Unknown camera: $camera'); + } +} + +/// An implementation of [ImagePickerPlatform] for iOS. +class ImagePickerIOS extends ImagePickerPlatform { + final ImagePickerApi _hostApi = ImagePickerApi(); + + /// Registers this class as the default platform implementation. + static void registerWith() { + ImagePickerPlatform.instance = ImagePickerIOS(); + } + + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final String? path = await _pickImageAsPath( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ), + ); + return path != null ? PickedFile(path) : null; + } + + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + final String? path = await _pickImageAsPath( + source: source, + options: options, + ); + return path != null ? XFile(path) : null; + } + + @override + Future?> pickMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + final List? paths = await _pickMultiImageAsPath( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ), + ), + ); + if (paths == null) { + return null; + } + + return paths.map((dynamic path) => PickedFile(path as String)).toList(); + } + + @override + Future> getMultiImageWithOptions({ + MultiImagePickerOptions options = const MultiImagePickerOptions(), + }) async { + final List? paths = await _pickMultiImageAsPath(options: options); + if (paths == null) { + return []; + } + + return paths.map((String path) => XFile(path)).toList(); + } + + Future?> _pickMultiImageAsPath({ + MultiImagePickerOptions options = const MultiImagePickerOptions(), + }) async { + final int? imageQuality = options.imageOptions.imageQuality; + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + + final double? maxWidth = options.imageOptions.maxWidth; + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + + final double? maxHeight = options.imageOptions.maxHeight; + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + + // TODO(stuartmorgan): Remove the cast once Pigeon supports non-nullable + // generics, https://github.com/flutter/flutter/issues/97848 + return (await _hostApi.pickMultiImage( + MaxSize(width: maxWidth, height: maxHeight), + imageQuality, + options.imageOptions.requestFullMetadata)) + ?.cast(); + } + + Future _pickImageAsPath({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) { + final int? imageQuality = options.imageQuality; + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + + final double? maxHeight = options.maxHeight; + final double? maxWidth = options.maxWidth; + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + + return _hostApi.pickImage( + SourceSpecification( + type: _convertSource(source), + camera: _convertCamera(options.preferredCameraDevice), + ), + MaxSize(width: maxWidth, height: maxHeight), + imageQuality, + options.requestFullMetadata, + ); + } + + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final String? path = await _pickVideoAsPath( + source: source, + maxDuration: maxDuration, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? PickedFile(path) : null; + } + + Future _pickVideoAsPath({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) { + return _hostApi.pickVideo( + SourceSpecification( + type: _convertSource(source), + camera: _convertCamera(preferredCameraDevice)), + maxDuration?.inSeconds); + } + + @override + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final String? path = await _pickImageAsPath( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ), + ); + return path != null ? XFile(path) : null; + } + + @override + Future?> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + final List? paths = await _pickMultiImageAsPath( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ), + ), + ); + if (paths == null) { + return null; + } + + return paths.map((String path) => XFile(path)).toList(); + } + + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final String? path = await _pickVideoAsPath( + source: source, + maxDuration: maxDuration, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? XFile(path) : null; + } +} diff --git a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart new file mode 100644 index 000000000000..5f8768ba8cc1 --- /dev/null +++ b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart @@ -0,0 +1,197 @@ +// 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.0.3), 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 +// @dart = 2.12 +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'; + +enum SourceCamera { + rear, + front, +} + +enum SourceType { + camera, + gallery, +} + +class MaxSize { + MaxSize({ + this.width, + this.height, + }); + + double? width; + double? height; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['width'] = width; + pigeonMap['height'] = height; + return pigeonMap; + } + + static MaxSize decode(Object message) { + final Map pigeonMap = message as Map; + return MaxSize( + width: pigeonMap['width'] as double?, + height: pigeonMap['height'] as double?, + ); + } +} + +class SourceSpecification { + SourceSpecification({ + required this.type, + this.camera, + }); + + SourceType type; + SourceCamera? camera; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['type'] = type.index; + pigeonMap['camera'] = camera?.index; + return pigeonMap; + } + + static SourceSpecification decode(Object message) { + final Map pigeonMap = message as Map; + return SourceSpecification( + type: SourceType.values[pigeonMap['type']! as int], + camera: pigeonMap['camera'] != null + ? SourceCamera.values[pigeonMap['camera']! as int] + : null, + ); + } +} + +class _ImagePickerApiCodec extends StandardMessageCodec { + const _ImagePickerApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is MaxSize) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return MaxSize.decode(readValue(buffer)!); + + case 129: + return SourceSpecification.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class ImagePickerApi { + /// Constructor for [ImagePickerApi]. 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. + ImagePickerApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _ImagePickerApiCodec(); + + Future pickImage(SourceSpecification arg_source, MaxSize arg_maxSize, + int? arg_imageQuality, bool arg_requestFullMetadata) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickImage', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel.send([ + arg_source, + arg_maxSize, + arg_imageQuality, + arg_requestFullMetadata + ]) 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 String?); + } + } + + Future?> pickMultiImage(MaxSize arg_maxSize, + int? arg_imageQuality, bool arg_requestFullMetadata) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMultiImage', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel.send( + [arg_maxSize, arg_imageQuality, arg_requestFullMetadata]) + 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 List?)?.cast(); + } + } + + Future pickVideo( + SourceSpecification arg_source, int? arg_maxDurationSeconds) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickVideo', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_source, arg_maxDurationSeconds]) + 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 String?); + } + } +} diff --git a/packages/image_picker/image_picker_ios/pigeons/copyright.txt b/packages/image_picker/image_picker_ios/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/image_picker/image_picker_ios/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/image_picker/image_picker_ios/pigeons/messages.dart b/packages/image_picker/image_picker_ios/pigeons/messages.dart new file mode 100644 index 000000000000..dd8a0f0c0834 --- /dev/null +++ b/packages/image_picker/image_picker_ios/pigeons/messages.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. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + dartTestOut: 'test/test_api.dart', + objcHeaderOut: 'ios/Classes/messages.g.h', + objcSourceOut: 'ios/Classes/messages.g.m', + objcOptions: ObjcOptions( + prefix: 'FLT', + ), + copyrightHeader: 'pigeons/copyright.txt', +)) +class MaxSize { + MaxSize(this.width, this.height); + double? width; + double? height; +} + +// Corresponds to `CameraDevice` from the platform interface package. +enum SourceCamera { rear, front } + +// Corresponds to `ImageSource` from the platform interface package. +enum SourceType { camera, gallery } + +class SourceSpecification { + SourceSpecification(this.type, this.camera); + SourceType type; + SourceCamera? camera; +} + +@HostApi(dartHostTestHandler: 'TestHostImagePickerApi') +abstract class ImagePickerApi { + @async + @ObjCSelector('pickImageWithSource:maxSize:quality:fullMetadata:') + String? pickImage(SourceSpecification source, MaxSize maxSize, + int? imageQuality, bool requestFullMetadata); + @async + @ObjCSelector('pickMultiImageWithMaxSize:quality:fullMetadata:') + List? pickMultiImage( + MaxSize maxSize, int? imageQuality, bool requestFullMetadata); + @async + @ObjCSelector('pickVideoWithSource:maxDuration:') + String? pickVideo(SourceSpecification source, int? maxDurationSeconds); +} diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml new file mode 100755 index 000000000000..6c78b2340ed0 --- /dev/null +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -0,0 +1,28 @@ +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+1 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: image_picker + platforms: + ios: + dartPluginClass: ImagePickerIOS + pluginClass: FLTImagePickerPlugin + +dependencies: + flutter: + sdk: flutter + image_picker_platform_interface: ^2.6.1 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.0 + pigeon: ^3.0.2 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 new file mode 100644 index 000000000000..b20025770ad1 --- /dev/null +++ b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart @@ -0,0 +1,1458 @@ +// 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_test/flutter_test.dart'; +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'; + +@immutable +class _LoggedMethodCall { + const _LoggedMethodCall(this.name, {required this.arguments}); + final String name; + final Map arguments; + + @override + bool operator ==(Object other) { + return other is _LoggedMethodCall && + name == other.name && + mapEquals(arguments, other.arguments); + } + + @override + int get hashCode => Object.hash(name, arguments); + + @override + String toString() { + return 'MethodCall: $name $arguments'; + } +} + +class _ApiLogger implements TestHostImagePickerApi { + // The value to return from future calls. + dynamic returnValue = ''; + final List<_LoggedMethodCall> calls = <_LoggedMethodCall>[]; + + @override + Future pickImage( + SourceSpecification source, + MaxSize maxSize, + int? imageQuality, + bool requestFullMetadata, + ) async { + // Flatten arguments for easy comparison. + calls.add(_LoggedMethodCall('pickImage', arguments: { + 'source': source.type, + 'cameraDevice': source.camera, + 'maxWidth': maxSize.width, + 'maxHeight': maxSize.height, + 'imageQuality': imageQuality, + 'requestFullMetadata': requestFullMetadata, + })); + return returnValue as String?; + } + + @override + Future?> pickMultiImage( + MaxSize maxSize, + int? imageQuality, + bool requestFullMetadata, + ) async { + calls.add(_LoggedMethodCall('pickMultiImage', arguments: { + 'maxWidth': maxSize.width, + 'maxHeight': maxSize.height, + 'imageQuality': imageQuality, + 'requestFullMetadata': requestFullMetadata, + })); + return returnValue as List?; + } + + @override + Future pickVideo( + SourceSpecification source, int? maxDurationSeconds) async { + calls.add(_LoggedMethodCall('pickVideo', arguments: { + 'source': source.type, + 'cameraDevice': source.camera, + 'maxDuration': maxDurationSeconds, + })); + return returnValue as String?; + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final ImagePickerIOS picker = ImagePickerIOS(); + late _ApiLogger log; + + setUp(() { + log = _ApiLogger(); + TestHostImagePickerApi.setup(log); + }); + + test('registration', () async { + ImagePickerIOS.registerWith(); + expect(ImagePickerPlatform.instance, isA()); + }); + + group('#pickImage', () { + test('passes the image source argument correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage(source: ImageSource.gallery); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.gallery, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickImage( + source: ImageSource.camera, + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('does not accept a invalid imageQuality argument', () { + expect( + () => picker.pickImage(imageQuality: -1, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: 101, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: -1, source: ImageSource.camera), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: 101, source: ImageSource.camera), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.pickImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + log.returnValue = null; + + expect(await picker.pickImage(source: ImageSource.gallery), isNull); + expect(await picker.pickImage(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.pickImage(source: ImageSource.camera); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.pickImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.front, + 'requestFullMetadata': true, + }), + ], + ); + }); + }); + + group('#pickMultiImage', () { + test('calls the method correctly', () async { + log.returnValue = ['0', '1']; + await picker.pickMultiImage(); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + log.returnValue = ['0', '1']; + await picker.pickMultiImage(); + await picker.pickMultiImage( + maxWidth: 10.0, + ); + await picker.pickMultiImage( + maxHeight: 10.0, + ); + await picker.pickMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickMultiImage( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickMultiImage( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.pickMultiImage(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickMultiImage(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + expect( + () => picker.pickMultiImage(imageQuality: -1), + throwsArgumentError, + ); + + expect( + () => picker.pickMultiImage(imageQuality: 101), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + log.returnValue = null; + + expect(await picker.pickMultiImage(), isNull); + }); + }); + + group('#pickVideo', () { + test('passes the image source argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo(source: ImageSource.gallery); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'cameraDevice': SourceCamera.rear, + 'maxDuration': null, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.gallery, + 'cameraDevice': SourceCamera.rear, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10), + ); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1), + ); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1), + ); + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': null, + 'cameraDevice': SourceCamera.rear, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': 10, + 'cameraDevice': SourceCamera.rear, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': 60, + 'cameraDevice': SourceCamera.rear, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': 3600, + 'cameraDevice': SourceCamera.rear, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + log.returnValue = null; + + expect(await picker.pickVideo(source: ImageSource.gallery), isNull); + expect(await picker.pickVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.pickVideo(source: ImageSource.camera); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'cameraDevice': SourceCamera.rear, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.pickVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front, + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': null, + 'cameraDevice': SourceCamera.front, + }), + ], + ); + }); + }); + + group('#getImage', () { + test('passes the image source argument correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage(source: ImageSource.gallery); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.gallery, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.getImage( + source: ImageSource.camera, + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('does not accept a invalid imageQuality argument', () { + expect( + () => picker.getImage(imageQuality: -1, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: 101, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: -1, source: ImageSource.camera), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: 101, source: ImageSource.camera), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.getImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.getImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + log.returnValue = null; + + expect(await picker.getImage(source: ImageSource.gallery), isNull); + expect(await picker.getImage(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getImage(source: ImageSource.camera); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.front, + 'requestFullMetadata': true, + }), + ], + ); + }); + }); + + group('#getMultiImage', () { + test('calls the method correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMultiImage(); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMultiImage(); + await picker.getMultiImage( + maxWidth: 10.0, + ); + await picker.getMultiImage( + maxHeight: 10.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMultiImage(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImage(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMultiImage(imageQuality: -1), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImage(imageQuality: 101), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + log.returnValue = null; + + expect(await picker.getMultiImage(), isNull); + expect(await picker.getMultiImage(), isNull); + }); + }); + + group('#getVideo', () { + test('passes the image source argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo(source: ImageSource.gallery); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'cameraDevice': SourceCamera.rear, + 'maxDuration': null, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.gallery, + 'cameraDevice': SourceCamera.rear, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10), + ); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1), + ); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1), + ); + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': null, + 'cameraDevice': SourceCamera.rear, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': 10, + 'cameraDevice': SourceCamera.rear, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': 60, + 'cameraDevice': SourceCamera.rear, + }), + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': 3600, + 'cameraDevice': SourceCamera.rear, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + log.returnValue = null; + + expect(await picker.getVideo(source: ImageSource.gallery), isNull); + expect(await picker.getVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getVideo(source: ImageSource.camera); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'cameraDevice': SourceCamera.rear, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front, + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'maxDuration': null, + 'cameraDevice': SourceCamera.front, + }), + ], + ); + }); + }); + + group('#getImageFromSource', () { + test('passes the image source argument correctly', () async { + await picker.getImageFromSource(source: ImageSource.camera); + await picker.getImageFromSource(source: ImageSource.gallery); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.gallery, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.getImageFromSource(source: ImageSource.camera); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxWidth: 10.0), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxHeight: 10.0), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxWidth: 10.0, + maxHeight: 20.0, + ), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxWidth: 10.0, + imageQuality: 70, + ), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxHeight: 10.0, + imageQuality: 70, + ), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('does not accept a invalid imageQuality argument', () { + expect( + () => picker.getImageFromSource( + source: ImageSource.gallery, + options: const ImagePickerOptions(imageQuality: -1), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.gallery, + options: const ImagePickerOptions(imageQuality: 101), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(imageQuality: -1), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(imageQuality: 101), + ), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxWidth: -1.0), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxHeight: -1.0), + ), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + log.returnValue = null; + + expect( + await picker.getImageFromSource(source: ImageSource.gallery), isNull); + expect( + await picker.getImageFromSource(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getImageFromSource(source: ImageSource.camera); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getImageFromSource( + source: ImageSource.camera, + options: + const ImagePickerOptions(preferredCameraDevice: CameraDevice.front), + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.front, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('Request full metadata argument defaults to true', () async { + await picker.getImageFromSource(source: ImageSource.gallery); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.gallery, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the request full metadata argument correctly', () async { + await picker.getImageFromSource( + source: ImageSource.gallery, + options: const ImagePickerOptions(requestFullMetadata: false), + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.gallery, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear, + 'requestFullMetadata': false, + }), + ], + ); + }); + }); + + group('#getMultiImageWithOptions', () { + test('calls the method correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMultiImageWithOptions(); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMultiImageWithOptions(); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(maxWidth: 10.0), + ), + ); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(maxHeight: 10.0), + ), + ); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(maxWidth: 10.0, maxHeight: 20.0), + ), + ); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(maxWidth: 10.0, imageQuality: 70), + ), + ); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(maxHeight: 10.0, imageQuality: 70), + ), + ); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + ), + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(maxWidth: -1.0), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(maxHeight: -1.0), + ), + ), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(imageQuality: -1), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(imageQuality: 101), + ), + ), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + log.returnValue = null; + + expect(await picker.getMultiImageWithOptions(), isEmpty); + }); + + test('Request full metadata argument defaults to true', () async { + log.returnValue = ['0', '1']; + await picker.getMultiImageWithOptions(); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('Passes the request full metadata argument correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(requestFullMetadata: false), + ), + ); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': false, + }), + ], + ); + }); + }); +} diff --git a/packages/image_picker/image_picker_ios/test/test_api.dart b/packages/image_picker/image_picker_ios/test/test_api.dart new file mode 100644 index 000000000000..1e44f600f57d --- /dev/null +++ b/packages/image_picker/image_picker_ios/test/test_api.dart @@ -0,0 +1,136 @@ +// 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.0.3), 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 +// ignore_for_file: avoid_relative_lib_imports +// @dart = 2.12 +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; + +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +// Manually changed due to https://github.com/flutter/flutter/issues/97744 +import 'package:image_picker_ios/src/messages.g.dart'; + +class _TestHostImagePickerApiCodec extends StandardMessageCodec { + const _TestHostImagePickerApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is MaxSize) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return MaxSize.decode(readValue(buffer)!); + + case 129: + return SourceSpecification.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestHostImagePickerApi { + static const MessageCodec codec = _TestHostImagePickerApiCodec(); + + Future pickImage(SourceSpecification source, MaxSize maxSize, + int? imageQuality, bool requestFullMetadata); + Future?> pickMultiImage( + MaxSize maxSize, int? imageQuality, bool requestFullMetadata); + Future pickVideo( + SourceSpecification source, int? maxDurationSeconds); + static void setup(TestHostImagePickerApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickImage', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImage was null.'); + final List args = (message as List?)!; + final SourceSpecification? arg_source = + (args[0] as SourceSpecification?); + assert(arg_source != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImage was null, expected non-null SourceSpecification.'); + final MaxSize? arg_maxSize = (args[1] as MaxSize?); + assert(arg_maxSize != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImage was null, expected non-null MaxSize.'); + final int? arg_imageQuality = (args[2] as int?); + final bool? arg_requestFullMetadata = (args[3] as bool?); + assert(arg_requestFullMetadata != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImage was null, expected non-null bool.'); + final String? output = await api.pickImage(arg_source!, arg_maxSize!, + arg_imageQuality, arg_requestFullMetadata!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMultiImage', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMultiImage was null.'); + final List args = (message as List?)!; + final MaxSize? arg_maxSize = (args[0] as MaxSize?); + assert(arg_maxSize != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMultiImage was null, expected non-null MaxSize.'); + final int? arg_imageQuality = (args[1] as int?); + final bool? arg_requestFullMetadata = (args[2] as bool?); + assert(arg_requestFullMetadata != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMultiImage was null, expected non-null bool.'); + final List? output = await api.pickMultiImage( + arg_maxSize!, arg_imageQuality, arg_requestFullMetadata!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickVideo', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideo was null.'); + final List args = (message as List?)!; + final SourceSpecification? arg_source = + (args[0] as SourceSpecification?); + assert(arg_source != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideo was null, expected non-null SourceSpecification.'); + final int? arg_maxDurationSeconds = (args[1] as int?); + final String? output = + await api.pickVideo(arg_source!, arg_maxDurationSeconds); + return {'result': output}; + }); + } + } + } +} diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 9f6d1749c671..05b03a37cb98 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,28 @@ +## 2.6.2 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 2.6.1 + +* Exports new types added for `getMultiImageWithOptions` in 2.6.0. + +## 2.6.0 + +* Deprecates `getMultiImage` in favor of a new method `getMultiImageWithOptions`. + * Adds `requestFullMetadata` option that allows disabling extra permission requests + on certain platforms. + * Moves optional image picking parameters to `MultiImagePickerOptions` class. + +## 2.5.0 + +* Deprecates `getImage` in favor of a new method `getImageFromSource`. + * Adds `requestFullMetadata` option that allows disabling extra permission requests + on certain platforms. + * Moves optional image picking parameters to `ImagePickerOptions` class. +* Minor fixes for new analysis options. + ## 2.4.4 * Internal code cleanup for stricter analysis options. diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart index e1e6082c8047..c2c39f93fe18 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart @@ -7,7 +7,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import '../../image_picker_platform_interface.dart'; const MethodChannel _channel = MethodChannel('plugins.flutter.io/image_picker'); @@ -57,6 +57,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, + bool requestFullMetadata = true, }) { if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { throw ArgumentError.value( @@ -77,6 +78,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { 'maxWidth': maxWidth, 'maxHeight': maxHeight, 'imageQuality': imageQuality, + 'requestFullMetadata': requestFullMetadata, }, ); } @@ -87,6 +89,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxHeight, int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, + bool requestFullMetadata = true, }) { if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { throw ArgumentError.value( @@ -108,7 +111,8 @@ class MethodChannelImagePicker extends ImagePickerPlatform { 'maxWidth': maxWidth, 'maxHeight': maxHeight, 'imageQuality': imageQuality, - 'cameraDevice': preferredCameraDevice.index + 'cameraDevice': preferredCameraDevice.index, + 'requestFullMetadata': requestFullMetadata, }, ); } @@ -197,6 +201,22 @@ class MethodChannelImagePicker extends ImagePickerPlatform { return path != null ? XFile(path) : null; } + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + final String? path = await _getImagePath( + source: source, + maxHeight: options.maxHeight, + maxWidth: options.maxWidth, + imageQuality: options.imageQuality, + preferredCameraDevice: options.preferredCameraDevice, + requestFullMetadata: options.requestFullMetadata, + ); + return path != null ? XFile(path) : null; + } + @override Future?> getMultiImage({ double? maxWidth, @@ -215,6 +235,23 @@ class MethodChannelImagePicker extends ImagePickerPlatform { return paths.map((dynamic path) => XFile(path as String)).toList(); } + @override + Future> getMultiImageWithOptions({ + MultiImagePickerOptions options = const MultiImagePickerOptions(), + }) async { + final List? paths = await _getMultiImagePath( + maxWidth: options.imageOptions.maxWidth, + maxHeight: options.imageOptions.maxHeight, + imageQuality: options.imageOptions.imageQuality, + requestFullMetadata: options.imageOptions.requestFullMetadata, + ); + if (paths == null) { + return []; + } + + return paths.map((dynamic path) => XFile(path as String)).toList(); + } + @override Future getVideo({ required ImageSource source, diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart index 8f02e1683267..9572742e62e0 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart @@ -5,10 +5,11 @@ import 'dart:async'; import 'package:cross_file/cross_file.dart'; -import 'package:image_picker_platform_interface/src/method_channel/method_channel_image_picker.dart'; -import 'package:image_picker_platform_interface/src/types/types.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import '../method_channel/method_channel_image_picker.dart'; +import '../types/types.dart'; + /// The interface that implementations of image_picker must implement. /// /// Platform implementations should extend this class rather than implement it as `image_picker` @@ -146,6 +147,8 @@ abstract class ImagePickerPlatform extends PlatformInterface { throw UnimplementedError('retrieveLostData() has not been implemented.'); } + /// This method is deprecated in favor of [getImageFromSource] and will be removed in a future update. + /// /// Returns an [XFile] with the image that was picked. /// /// The `source` argument controls where the image comes from. This can @@ -184,6 +187,8 @@ abstract class ImagePickerPlatform extends PlatformInterface { throw UnimplementedError('getImage() has not been implemented.'); } + /// This method is deprecated in favor of [getMultiImageWithOptions] and will be removed in a future update. + /// /// Returns a [List] with the images that were picked. /// /// The images come from the [ImageSource.gallery]. @@ -251,4 +256,53 @@ abstract class ImagePickerPlatform extends PlatformInterface { Future getLostData() { throw UnimplementedError('getLostData() has not been implemented.'); } + + /// Returns an [XFile] with the image that was picked. + /// + /// The `source` argument controls where the image comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// The `options` argument controls additional settings that can be used when + /// picking an image. See [ImagePickerOptions] for more details. + /// + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and + /// above only support HEIC images if used in addition to a size modification, + /// of which the usage is explained in [ImagePickerOptions]. + /// + /// In Android, the MainActivity can be destroyed for various reasons. If that + /// happens, the result will be lost in this call. You can then call [getLostData] + /// when your app relaunches to retrieve the lost data. + /// + /// If no images were picked, the return value is null. + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) { + return getImage( + source: source, + maxHeight: options.maxHeight, + maxWidth: options.maxWidth, + imageQuality: options.imageQuality, + preferredCameraDevice: options.preferredCameraDevice, + ); + } + + /// Returns a [List] with the images that were picked. + /// + /// The images come from the [ImageSource.gallery]. + /// + /// The `options` argument controls additional settings that can be used when + /// picking an image. See [MultiImagePickerOptions] for more details. + /// + /// If no images were picked, returns an empty list. + Future> getMultiImageWithOptions({ + MultiImagePickerOptions options = const MultiImagePickerOptions(), + }) async { + final List? pickedImages = await getMultiImage( + maxWidth: options.imageOptions.maxWidth, + maxHeight: options.imageOptions.maxHeight, + imageQuality: options.imageOptions.imageQuality, + ); + return pickedImages ?? []; + } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart new file mode 100644 index 000000000000..2cc01c92da1d --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart @@ -0,0 +1,41 @@ +// 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. + +/// Specifies image-specific options for picking. +class ImageOptions { + /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality] + /// and [requestFullMetadata]. + const ImageOptions({ + this.maxHeight, + this.maxWidth, + this.imageQuality, + this.requestFullMetadata = true, + }); + + /// The maximum width of the image, in pixels. + /// + /// If null, the image will only be resized if [maxHeight] is specified. + final double? maxWidth; + + /// The maximum height of the image, in pixels. + /// + /// If null, the image will only be resized if [maxWidth] is specified. + final double? maxHeight; + + /// Modifies the quality of the image, ranging from 0-100 where 100 is the + /// original/max quality. + /// + /// Compression is only supported for certain image types such as JPEG. If + /// compression is not supported for the image that is picked, a warning + /// message will be logged. + /// + /// If null, the image will be returned with the original quality. + final int? imageQuality; + + /// If true, requests full image metadata, which may require extra permissions + /// on some platforms, (e.g., NSPhotoLibraryUsageDescription on iOS). + // + // Defaults to true. + final bool requestFullMetadata; +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart new file mode 100644 index 000000000000..0d85c918f649 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart @@ -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. + +import 'types.dart'; + +/// Specifies options for picking a single image from the device's camera or gallery. +class ImagePickerOptions { + /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality], + /// [referredCameraDevice] and [requestFullMetadata]. + const ImagePickerOptions({ + this.maxHeight, + this.maxWidth, + this.imageQuality, + this.preferredCameraDevice = CameraDevice.rear, + this.requestFullMetadata = true, + }); + + /// The maximum width of the image, in pixels. + /// + /// If null, the image will only be resized if [maxHeight] is specified. + final double? maxWidth; + + /// The maximum height of the image, in pixels. + /// + /// If null, the image will only be resized if [maxWidth] is specified. + final double? maxHeight; + + /// Modifies the quality of the image, ranging from 0-100 where 100 is the + /// original/max quality. + /// + /// Compression is only supported for certain image types such as JPEG. If + /// compression is not supported for the image that is picked, a warning + /// message will be logged. + /// + /// If null, the image will be returned with the original quality. + final int? imageQuality; + + /// Used to specify the camera to use when the `source` is [ImageSource.camera]. + /// + /// Ignored if the source is not [ImageSource.camera], or the chosen camera is not + /// supported on the device. Defaults to [CameraDevice.rear]. + final CameraDevice preferredCameraDevice; + + /// If true, requests full image metadata, which may require extra permissions + /// on some platforms, (e.g., NSPhotoLibraryUsageDescription on iOS). + // + // Defaults to true. + final bool requestFullMetadata; +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart index 65f5d7e15c90..10af812a3109 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart @@ -4,7 +4,8 @@ import 'package:cross_file/cross_file.dart'; import 'package:flutter/services.dart'; -import 'package:image_picker_platform_interface/src/types/types.dart'; + +import 'types.dart'; /// The response object of [ImagePicker.getLostData]. /// diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/multi_image_picker_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/multi_image_picker_options.dart new file mode 100644 index 000000000000..c860297ce33f --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/multi_image_picker_options.dart @@ -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. + +import 'image_options.dart'; + +/// Specifies options for picking multiple images from the device's gallery. +class MultiImagePickerOptions { + /// Creates an instance with the given [imageOptions]. + const MultiImagePickerOptions({ + this.imageOptions = const ImageOptions(), + }); + + /// The image-specific options for picking. + final ImageOptions imageOptions; +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart index 64f6a1f27538..ddd36b62c023 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart @@ -3,7 +3,8 @@ // found in the LICENSE file. import 'package:flutter/services.dart'; -import 'package:image_picker_platform_interface/src/types/types.dart'; + +import '../types.dart'; /// The response object of [ImagePicker.retrieveLostData]. /// diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart index 7f2844230287..fbe12e8e825a 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart @@ -3,8 +3,11 @@ // found in the LICENSE file. export 'camera_device.dart'; +export 'image_options.dart'; +export 'image_picker_options.dart'; export 'image_source.dart'; export 'lost_data_response.dart'; +export 'multi_image_picker_options.dart'; export 'picked_file/picked_file.dart'; export 'retrieve_type.dart'; diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 54fd17e47260..eb4d2b649eac 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/i issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.4.4 +version: 2.6.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.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 79d971f217f0..44980100742a 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 @@ -4,7 +4,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:image_picker_platform_interface/src/method_channel/method_channel_image_picker.dart'; @@ -40,14 +39,16 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 1, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); @@ -93,55 +94,62 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); }); - test('does not accept a invalid imageQuality argument', () { + test('does not accept an invalid imageQuality argument', () { expect( () => picker.pickImage(imageQuality: -1, source: ImageSource.gallery), throwsArgumentError, @@ -196,6 +204,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); @@ -215,6 +224,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, + 'requestFullMetadata': true, }), ], ); @@ -233,6 +243,7 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, + 'requestFullMetadata': true, }), ], ); @@ -272,36 +283,43 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, + 'requestFullMetadata': true, }), ], ); @@ -320,7 +338,7 @@ void main() { ); }); - test('does not accept a invalid imageQuality argument', () { + test('does not accept an invalid imageQuality argument', () { returnValue = ['0', '1']; expect( () => picker.pickMultiImage(imageQuality: -1), @@ -509,14 +527,16 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 1, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); @@ -562,55 +582,62 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); }); - test('does not accept a invalid imageQuality argument', () { + test('does not accept an invalid imageQuality argument', () { expect( () => picker.getImage(imageQuality: -1, source: ImageSource.gallery), throwsArgumentError, @@ -664,6 +691,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); @@ -683,6 +711,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, + 'requestFullMetadata': true, }), ], ); @@ -701,6 +730,7 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, + 'requestFullMetadata': true, }), ], ); @@ -740,36 +770,43 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, + 'requestFullMetadata': true, }), ], ); @@ -788,7 +825,7 @@ void main() { ); }); - test('does not accept a invalid imageQuality argument', () { + test('does not accept an invalid imageQuality argument', () { returnValue = ['0', '1']; expect( () => picker.getMultiImage(imageQuality: -1), @@ -979,5 +1016,465 @@ void main() { expect(picker.getLostData(), throwsAssertionError); }); }); + + group('#getImageFromSource', () { + test('passes the image source argument correctly', () async { + await picker.getImageFromSource(source: ImageSource.camera); + await picker.getImageFromSource(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.getImageFromSource(source: ImageSource.camera); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxWidth: 10.0), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxHeight: 10.0), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxWidth: 10.0, + maxHeight: 20.0, + ), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxWidth: 10.0, + imageQuality: 70, + ), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxHeight: 10.0, + imageQuality: 70, + ), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('does not accept an invalid imageQuality argument', () { + expect( + () => picker.getImageFromSource( + source: ImageSource.gallery, + options: const ImagePickerOptions(imageQuality: -1), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.gallery, + options: const ImagePickerOptions(imageQuality: 101), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(imageQuality: -1), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(imageQuality: 101), + ), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxWidth: -1.0), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxHeight: -1.0), + ), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel + .setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getImageFromSource(source: ImageSource.gallery), + isNull); + expect(await picker.getImageFromSource(source: ImageSource.camera), + isNull); + }); + + test('camera position defaults to back', () async { + await picker.getImageFromSource(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + preferredCameraDevice: CameraDevice.front, + ), + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the full metadata argument correctly', () async { + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(requestFullMetadata: false), + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': false, + }), + ], + ); + }); + }); + + group('#getMultiImageWithOptions', () { + test('calls the method correctly', () async { + returnValue = ['0', '1']; + await picker.getMultiImageWithOptions(); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the width, height and imageQuality arguments correctly', + () async { + returnValue = ['0', '1']; + await picker.getMultiImageWithOptions(); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(maxWidth: 10.0), + ), + ); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(maxHeight: 10.0), + ), + ); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: 10.0, + maxHeight: 20.0, + ), + ), + ); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: 10.0, + imageQuality: 70, + ), + ), + ); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions( + maxHeight: 10.0, + imageQuality: 70, + ), + ), + ); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + ), + ); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(maxWidth: -1.0), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(maxHeight: -1.0), + ), + ), + throwsArgumentError, + ); + }); + + test('does not accept an invalid imageQuality argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(imageQuality: -1), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(imageQuality: 101), + ), + ), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel + .setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getMultiImage(), isNull); + expect(await picker.getMultiImage(), isNull); + }); + + test('Request full metadata argument defaults to true', () async { + returnValue = ['0', '1']; + await picker.getMultiImageWithOptions(); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the request full metadata argument correctly', () async { + returnValue = ['0', '1']; + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(requestFullMetadata: false), + ), + ); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': false, + }), + ], + ); + }); + }); }); } diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart index 3201d3adea41..3e6cd0e01ca6 100644 --- a/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart @@ -13,7 +13,7 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface. final String pathPrefix = Directory.current.path.endsWith('test') ? './assets/' : './test/assets/'; -final String path = pathPrefix + 'hello.txt'; +final String path = '${pathPrefix}hello.txt'; const String expectedStringContents = 'Hello, world!'; final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents)); final File textFile = File(path); diff --git a/packages/image_picker/image_picker_windows/AUTHORS b/packages/image_picker/image_picker_windows/AUTHORS new file mode 100644 index 000000000000..5db3d584e6bc --- /dev/null +++ b/packages/image_picker/image_picker_windows/AUTHORS @@ -0,0 +1,7 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +Alexandre Zollinger Chohfi \ No newline at end of file diff --git a/packages/image_picker/image_picker_windows/CHANGELOG.md b/packages/image_picker/image_picker_windows/CHANGELOG.md new file mode 100644 index 000000000000..427598760a4b --- /dev/null +++ b/packages/image_picker/image_picker_windows/CHANGELOG.md @@ -0,0 +1,18 @@ +## 0.1.0+3 + +* Changes XTypeGroup initialization from final to const. +* Updates minimum Flutter version to 2.10. + +## 0.1.0+2 + +* Minor fixes for new analysis options. + +## 0.1.0+1 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.1.0 + +* Initial Windows support. diff --git a/packages/image_picker/image_picker_windows/LICENSE b/packages/image_picker/image_picker_windows/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/image_picker/image_picker_windows/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/image_picker/image_picker_windows/README.md b/packages/image_picker/image_picker_windows/README.md new file mode 100644 index 000000000000..0b256411b2fc --- /dev/null +++ b/packages/image_picker/image_picker_windows/README.md @@ -0,0 +1,16 @@ +# image\_picker\_windows + +A Windows implementation of [`image_picker`][1]. + +### pickImage() +The arguments `source`, `maxWidth`, `maxHeight`, `imageQuality`, and `preferredCameraDevice` are not supported on Windows. + +### pickVideo() +The arguments `source`, `preferredCameraDevice`, and `maxDuration` are not supported on Windows. + +## Usage + +### Import the package + +This package is not yet [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), which means you need to add +not only the `image_picker`, as well as the `image_picker_windows`. \ No newline at end of file diff --git a/packages/image_picker/image_picker_windows/example/README.md b/packages/image_picker/image_picker_windows/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/image_picker/image_picker_windows/example/lib/main.dart b/packages/image_picker/image_picker_windows/example/lib/main.dart new file mode 100644 index 000000000000..e340a185bf3d --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/lib/main.dart @@ -0,0 +1,416 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Image Picker Demo', + home: MyHomePage(title: 'Image Picker Example'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, this.title}) : super(key: key); + + final String? title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + List? _imageFileList; + + // This must be called from within a setState() callback + void _setImageFileListFromFile(PickedFile? value) { + _imageFileList = value == null ? null : [value]; + } + + dynamic _pickImageError; + bool _isVideo = false; + + VideoPlayerController? _controller; + VideoPlayerController? _toBeDisposed; + String? _retrieveDataError; + + final ImagePickerPlatform _picker = ImagePickerPlatform.instance; + final TextEditingController maxWidthController = TextEditingController(); + final TextEditingController maxHeightController = TextEditingController(); + final TextEditingController qualityController = TextEditingController(); + + Future _playVideo(PickedFile? file) async { + if (file != null && mounted) { + await _disposeVideoController(); + final VideoPlayerController controller = + VideoPlayerController.file(File(file.path)); + _controller = controller; + await controller.setVolume(1.0); + await controller.initialize(); + await controller.setLooping(true); + await controller.play(); + setState(() {}); + } + } + + Future _handleMultiImagePicked(BuildContext? context) async { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List? pickedFileList = await _picker.pickMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + + Future _handleSingleImagePicked( + BuildContext? context, ImageSource source) async { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final PickedFile? pickedFile = await _picker.pickImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + + Future _onImageButtonPressed(ImageSource source, + {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); + } + } + + @override + void deactivate() { + if (_controller != null) { + _controller!.setVolume(0.0); + _controller!.pause(); + } + super.deactivate(); + } + + @override + void dispose() { + _disposeVideoController(); + maxWidthController.dispose(); + maxHeightController.dispose(); + qualityController.dispose(); + super.dispose(); + } + + Future _disposeVideoController() async { + if (_toBeDisposed != null) { + await _toBeDisposed!.dispose(); + } + _toBeDisposed = _controller; + _controller = null; + } + + Widget _previewVideo() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_controller == null) { + return const Text( + 'You have not yet picked a video', + textAlign: TextAlign.center, + ); + } + return Padding( + padding: const EdgeInsets.all(10.0), + child: AspectRatioVideo(_controller), + ); + } + + Widget _previewImages() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_imageFileList != null) { + return Semantics( + label: 'image_picker_example_picked_images', + child: ListView.builder( + key: UniqueKey(), + itemBuilder: (BuildContext context, int index) { + return Semantics( + label: 'image_picker_example_picked_image', + child: Image.file(File(_imageFileList![index].path)), + ); + }, + itemCount: _imageFileList!.length, + ), + ); + } else if (_pickImageError != null) { + return Text( + 'Pick image error: $_pickImageError', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + + Widget _handlePreview() { + if (_isVideo) { + return _previewVideo(); + } else { + return _previewImages(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title!), + ), + body: Center( + child: _handlePreview(), + ), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Semantics( + label: 'image_picker_example_from_gallery', + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'image0', + tooltip: 'Pick Image from gallery', + child: const Icon(Icons.photo), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); + }, + heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.gallery); + }, + heroTag: 'video0', + tooltip: 'Pick Video from gallery', + child: const Icon(Icons.video_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.camera); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), + ), + ], + ), + ); + } + + Text? _getRetrieveErrorWidget() { + if (_retrieveDataError != null) { + final Text result = Text(_retrieveDataError!); + _retrieveDataError = null; + return result; + } + return null; + } + + Future _displayPickImageDialog( + BuildContext context, OnPickImageCallback onPick) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Add optional parameters'), + content: Column( + children: [ + TextField( + controller: maxWidthController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxWidth if desired'), + ), + TextField( + controller: maxHeightController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxHeight if desired'), + ), + TextField( + controller: qualityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter quality if desired'), + ), + ], + ), + actions: [ + TextButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('PICK'), + onPressed: () { + final double? width = maxWidthController.text.isNotEmpty + ? double.parse(maxWidthController.text) + : null; + final double? height = maxHeightController.text.isNotEmpty + ? double.parse(maxHeightController.text) + : null; + final int? quality = qualityController.text.isNotEmpty + ? int.parse(qualityController.text) + : null; + onPick(width, height, quality); + Navigator.of(context).pop(); + }), + ], + ); + }); + } +} + +typedef OnPickImageCallback = void Function( + double? maxWidth, double? maxHeight, int? quality); + +class AspectRatioVideo extends StatefulWidget { + const AspectRatioVideo(this.controller, {Key? key}) : super(key: key); + + final VideoPlayerController? controller; + + @override + AspectRatioVideoState createState() => AspectRatioVideoState(); +} + +class AspectRatioVideoState extends State { + VideoPlayerController? get controller => widget.controller; + bool initialized = false; + + void _onVideoControllerUpdate() { + if (!mounted) { + return; + } + if (initialized != controller!.value.isInitialized) { + initialized = controller!.value.isInitialized; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + controller!.addListener(_onVideoControllerUpdate); + } + + @override + void dispose() { + controller!.removeListener(_onVideoControllerUpdate); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (initialized) { + return Center( + child: AspectRatio( + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller!), + ), + ); + } else { + return Container(); + } + } +} diff --git a/packages/image_picker/image_picker_windows/example/pubspec.yaml b/packages/image_picker/image_picker_windows/example/pubspec.yaml new file mode 100644 index 000000000000..b87000a6caff --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: example +description: Example for image_picker_windows implementation. +publish_to: 'none' +version: 1.0.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + flutter: + sdk: flutter + image_picker_platform_interface: ^2.4.3 + image_picker_windows: + # When depending on this package from a real application you should use: + # image_picker_windows: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: .. + video_player: ^2.1.4 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/image_picker/image_picker_windows/example/windows/.gitignore b/packages/image_picker/image_picker_windows/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/image_picker/image_picker_windows/example/windows/CMakeLists.txt b/packages/image_picker/image_picker_windows/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..1633297a0c7c --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +set(BINARY_NAME "example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/image_picker/image_picker_windows/example/windows/flutter/CMakeLists.txt b/packages/image_picker/image_picker_windows/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..b2e4bd8d658b --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/image_picker/image_picker_windows/example/windows/flutter/generated_plugins.cmake b/packages/image_picker/image_picker_windows/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..a423a02476a2 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/CMakeLists.txt b/packages/image_picker/image_picker_windows/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..de2d8916b72b --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/Runner.rc b/packages/image_picker/image_picker_windows/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..5fdea291cf19 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/flutter_window.cpp b/packages/image_picker/image_picker_windows/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..8254bd9ff3c1 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/flutter_window.cpp @@ -0,0 +1,65 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/flutter_window.h b/packages/image_picker/image_picker_windows/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..f1fc669093d0 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/flutter_window.h @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/main.cpp b/packages/image_picker/image_picker_windows/example/windows/runner/main.cpp new file mode 100644 index 000000000000..df379fa0be93 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/main.cpp @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t* command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/resource.h b/packages/image_picker/image_picker_windows/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/resources/app_icon.ico b/packages/image_picker/image_picker_windows/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/image_picker/image_picker_windows/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/runner.exe.manifest b/packages/image_picker/image_picker_windows/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/utils.cpp b/packages/image_picker/image_picker_windows/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..fb7e945a63b7 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/utils.cpp @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE* unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = + ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, + nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/utils.h b/packages/image_picker/image_picker_windows/example/windows/runner/utils.h new file mode 100644 index 000000000000..bd81e1e02338 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/utils.h @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/win32_window.cpp b/packages/image_picker/image_picker_windows/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..85aa3614e8ad --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/win32_window.cpp @@ -0,0 +1,241 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { ++g_active_window_count; } + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { return window_handle_; } + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/packages/image_picker/image_picker_windows/example/windows/runner/win32_window.h b/packages/image_picker/image_picker_windows/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..d2a730052223 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/runner/win32_window.h @@ -0,0 +1,99 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart new file mode 100644 index 000000000000..90e86bf486b4 --- /dev/null +++ b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart @@ -0,0 +1,167 @@ +// 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_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_windows/file_selector_windows.dart'; +import 'package:flutter/foundation.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +/// The Windows implementation of [ImagePickerPlatform]. +/// +/// This class implements the `package:image_picker` functionality for +/// Windows. +class ImagePickerWindows extends ImagePickerPlatform { + /// Constructs a ImagePickerWindows. + ImagePickerWindows(); + + /// List of image extensions used when picking images + @visibleForTesting + static const List imageFormats = [ + 'jpg', + 'jpeg', + 'png', + 'bmp', + 'webp', + 'gif', + 'tif', + 'tiff', + 'apng' + ]; + + /// List of video extensions used when picking videos + @visibleForTesting + static const List videoFormats = [ + 'mov', + 'wmv', + 'mkv', + 'mp4', + 'webm', + 'avi', + 'mpeg', + 'mpg' + ]; + + /// The file selector used to prompt the user to select images or videos. + @visibleForTesting + static FileSelectorPlatform fileSelector = FileSelectorWindows(); + + /// Registers this class as the default instance of [ImagePickerPlatform]. + static void registerWith() { + ImagePickerPlatform.instance = ImagePickerWindows(); + } + + // `maxWidth`, `maxHeight`, `imageQuality` and `preferredCameraDevice` + // arguments are not supported on Windows. If any of these arguments + // is supplied, it'll be silently ignored by the Windows version of + // the plugin. `source` is not implemented for `ImageSource.camera` + // and will throw an exception. + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final XFile? file = await getImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice); + if (file != null) { + return PickedFile(file.path); + } + return null; + } + + // `preferredCameraDevice` and `maxDuration` arguments are not + // supported on Windows. If any of these arguments is supplied, + // it'll be silently ignored by the Windows version of the plugin. + // `source` is not implemented for `ImageSource.camera` and will + // throw an exception. + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final XFile? file = await getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + if (file != null) { + return PickedFile(file.path); + } + return null; + } + + // `maxWidth`, `maxHeight`, `imageQuality`, and `preferredCameraDevice` + // arguments are not supported on Windows. If any of these arguments + // is supplied, it'll be silently ignored by the Windows version + // of the plugin. `source` is not implemented for `ImageSource.camera` + // and will throw an exception. + @override + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + if (source != ImageSource.gallery) { + // TODO(azchohfi): Support ImageSource.camera. + // See https://github.com/flutter/flutter/issues/102115 + throw UnimplementedError( + 'ImageSource.gallery is currently the only supported source on Windows'); + } + const XTypeGroup typeGroup = + XTypeGroup(label: 'images', extensions: imageFormats); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; + } + + // `preferredCameraDevice` and `maxDuration` arguments are not + // supported on Windows. If any of these arguments is supplied, + // it'll be silently ignored by the Windows version of the plugin. + // `source` is not implemented for `ImageSource.camera` and will + // throw an exception. + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + if (source != ImageSource.gallery) { + // TODO(azchohfi): Support ImageSource.camera. + // See https://github.com/flutter/flutter/issues/102115 + throw UnimplementedError( + 'ImageSource.gallery is currently the only supported source on Windows'); + } + const XTypeGroup typeGroup = + XTypeGroup(label: 'videos', extensions: videoFormats); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; + } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not + // supported on Windows. If any of these arguments is supplied, + // it'll be silently ignored by the Windows version of the plugin. + @override + Future> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + const XTypeGroup typeGroup = + XTypeGroup(label: 'images', extensions: imageFormats); + final List files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + return files; + } +} diff --git a/packages/image_picker/image_picker_windows/pubspec.yaml b/packages/image_picker/image_picker_windows/pubspec.yaml new file mode 100644 index 000000000000..5d6988cc2931 --- /dev/null +++ b/packages/image_picker/image_picker_windows/pubspec.yaml @@ -0,0 +1,29 @@ +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 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: image_picker + platforms: + windows: + dartPluginClass: ImagePickerWindows + +dependencies: + file_selector_platform_interface: ^2.2.0 + file_selector_windows: ^0.8.2 + flutter: + sdk: flutter + image_picker_platform_interface: ^2.4.3 + +dev_dependencies: + build_runner: ^2.1.5 + flutter_test: + sdk: flutter + mockito: ^5.0.16 diff --git a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart new file mode 100644 index 000000000000..c3df2d80679f --- /dev/null +++ b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart @@ -0,0 +1,128 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:image_picker_windows/image_picker_windows.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'image_picker_windows_test.mocks.dart'; + +@GenerateMocks([FileSelectorPlatform]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$ImagePickerWindows()', () { + final ImagePickerWindows plugin = ImagePickerWindows(); + late MockFileSelectorPlatform mockFileSelectorPlatform; + + setUp(() { + mockFileSelectorPlatform = MockFileSelectorPlatform(); + + when(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .thenAnswer((_) async => null); + + when(mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .thenAnswer((_) async => List.empty()); + + ImagePickerWindows.fileSelector = mockFileSelectorPlatform; + }); + + test('registered instance', () { + ImagePickerWindows.registerWith(); + expect(ImagePickerPlatform.instance, isA()); + }); + + group('images', () { + test('pickImage passes the accepted type groups correctly', () async { + await plugin.pickImage(source: ImageSource.gallery); + + expect( + verify(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) + .captured + .single[0] + .extensions, + ImagePickerWindows.imageFormats); + }); + + test('pickImage throws UnimplementedError when source is camera', + () async { + expect(() async => await plugin.pickImage(source: ImageSource.camera), + throwsA(isA())); + }); + + test('getImage passes the accepted type groups correctly', () async { + await plugin.getImage(source: ImageSource.gallery); + + expect( + verify(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) + .captured + .single[0] + .extensions, + ImagePickerWindows.imageFormats); + }); + + test('getImage throws UnimplementedError when source is camera', + () async { + expect(() async => await plugin.getImage(source: ImageSource.camera), + throwsA(isA())); + }); + + test('getMultiImage passes the accepted type groups correctly', () async { + await plugin.getMultiImage(); + + expect( + verify(mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) + .captured + .single[0] + .extensions, + ImagePickerWindows.imageFormats); + }); + }); + group('videos', () { + test('pickVideo passes the accepted type groups correctly', () async { + await plugin.pickVideo(source: ImageSource.gallery); + + expect( + verify(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) + .captured + .single[0] + .extensions, + ImagePickerWindows.videoFormats); + }); + + test('pickVideo throws UnimplementedError when source is camera', + () async { + expect(() async => await plugin.pickVideo(source: ImageSource.camera), + throwsA(isA())); + }); + + test('getVideo passes the accepted type groups correctly', () async { + await plugin.getVideo(source: ImageSource.gallery); + + expect( + verify(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) + .captured + .single[0] + .extensions, + ImagePickerWindows.videoFormats); + }); + + test('getVideo throws UnimplementedError when source is camera', + () async { + expect(() async => await plugin.getVideo(source: ImageSource.camera), + throwsA(isA())); + }); + }); + }); +} diff --git a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.mocks.dart b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.mocks.dart new file mode 100644 index 000000000000..be2dd2ac5768 --- /dev/null +++ b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.mocks.dart @@ -0,0 +1,78 @@ +// Mocks generated by Mockito 5.1.0 from annotations +// in image_picker_windows/example/windows/flutter/ephemeral/.plugin_symlinks/image_picker_windows/test/image_picker_windows_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i3; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart' + as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +/// A class which mocks [FileSelectorPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileSelectorPlatform extends _i1.Mock + implements _i2.FileSelectorPlatform { + MockFileSelectorPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.XFile?> openFile( + {List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText}) => + (super.noSuchMethod( + Invocation.method(#openFile, [], { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText + }), + returnValue: Future<_i2.XFile?>.value()) as _i3.Future<_i2.XFile?>); + @override + _i3.Future> openFiles( + {List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText}) => + (super.noSuchMethod( + Invocation.method(#openFiles, [], { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText + }), + returnValue: Future>.value(<_i2.XFile>[])) + as _i3.Future>); + @override + _i3.Future getSavePath( + {List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText}) => + (super.noSuchMethod( + Invocation.method(#getSavePath, [], { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #suggestedName: suggestedName, + #confirmButtonText: confirmButtonText + }), + returnValue: Future.value()) as _i3.Future); + @override + _i3.Future getDirectoryPath( + {String? initialDirectory, String? confirmButtonText}) => + (super.noSuchMethod( + Invocation.method(#getDirectoryPath, [], { + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText + }), + returnValue: Future.value()) as _i3.Future); +} diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md index 134fd9052a63..f38fe18a25d4 100644 --- a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -1,3 +1,35 @@ +## 3.0.8 + +* Updates minimum Flutter version to 2.10. +* Bumps minimum in_app_purchase_android to 0.2.3. + +## 3.0.7 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 3.0.6 + +* Ignores deprecation warnings for upcoming styleFrom button API changes. + +## 3.0.5 + +* Updates references to the obsolete master branch. + +## 3.0.4 + +* Minor fixes for new analysis options. + +## 3.0.3 + +* Removes unnecessary imports. +* Adds OS version support information to README. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 3.0.2 + +* Adds additional explanation on why it is important to complete a purchase. + ## 3.0.1 * Internal code cleanup for stricter analysis options. diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index b67f9e5b52ac..8986b9dea809 100644 --- a/packages/in_app_purchase/in_app_purchase/README.md +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -5,11 +5,15 @@ 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) or Google Play (on Android). +| | Android | iOS | +|-------------|---------|------| +| **Support** | SDK 16+ | 9.0+ | +

- An animated image of the iOS in-app purchase UI      - An animated image of the Android in-app purchase UI

@@ -32,8 +36,12 @@ your app with each store. Both stores have extensive guides: * [App Store documentation](https://developer.apple.com/in-app-purchase/) * [Google Play documentation](https://developer.android.com/google/play/billing/billing_overview) +> NOTE: Further in this document the App Store and Google Play will be referred +> to as "the store" or "the underlying store", except when a feature is specific +> to a particular store. + For a list of steps for configuring in-app purchases in both stores, see the -[example app README](https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/in_app_purchase/example/README.md). +[example app README](https://github.com/flutter/plugins/blob/main/packages/in_app_purchase/in_app_purchase/example/README.md). Once you've configured your in-app purchases in their respective stores, you can start using the plugin. Two basic options are available: @@ -190,11 +198,15 @@ if (_isConsumable(productDetails)) { ### Completing a purchase -The `InAppPurchase.purchaseStream` will send purchase updates after -you initiate the purchase flow using `InAppPurchase.buyConsumable` -or `InAppPurchase.buyNonConsumable`. After delivering the content to -the user, call `InAppPurchase.completePurchase` to tell the App Store -and Google Play that the purchase has been finished. +The `InAppPurchase.purchaseStream` will send purchase updates after initiating +the purchase flow using `InAppPurchase.buyConsumable` or +`InAppPurchase.buyNonConsumable`. After verifying the purchase receipt and the +delivering the content to the user it is important to call +`InAppPurchase.completePurchase` to tell the underlying store that the +purchase has been completed. Calling `InAppPurchase.completePurchase` will +inform the underlying store that the app verified and processed the +purchase and the store can proceed to finalize the transaction and bill +the end user's payment account. > **Warning:** Failure to call `InAppPurchase.completePurchase` and > get a successful response within 3 days of the purchase will result a refund. @@ -229,7 +241,7 @@ InAppPurchase.instance When the price of a subscription is changed the consumer will need to confirm that price change. If the consumer does not confirm the price change the subscription will not be auto-renewed. By default on both iOS and Android the consumer will -automatically get a popup to confirm the price change, but App developers can override this mechanism and show the popup on a later moment so it doesn't interrupt the critical flow of the App. This works different on the Apple App Store and on the Google Play Store. +automatically get a popup to confirm the price change, but App developers can override this mechanism and show the popup on a later moment so it doesn't interrupt the critical flow of the App. This works different for each of the stores. #### Google Play Store (Android) When the subscription price is raised, the consumer should approve the price change within 7 days. The official @@ -414,4 +426,4 @@ iosPlatformAddition.presentCodeRedemptionSheet(); ## Contributing to this plugin If you would like to contribute to the plugin, check out our -[contribution guide](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md). +[contribution guide](https://github.com/flutter/plugins/blob/main/CONTRIBUTING.md). diff --git a/packages/in_app_purchase/in_app_purchase/example/android/app/build.gradle b/packages/in_app_purchase/in_app_purchase/example/android/app/build.gradle index 772b8839be59..9bb39660cf05 100644 --- a/packages/in_app_purchase/in_app_purchase/example/android/app/build.gradle +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/build.gradle @@ -107,9 +107,9 @@ flutter { dependencies { implementation 'com.android.billingclient:billing:3.0.2' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:3.6.0' - testImplementation 'org.json:json:20180813' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:4.7.0' + 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/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/in_app_purchase/in_app_purchase/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/in_app_purchase/in_app_purchase/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/in_app_purchase/in_app_purchase/example/android/build.gradle b/packages/in_app_purchase/in_app_purchase/example/android/build.gradle index 0b4cf534e0aa..c21bff8e0a2f 100644 --- a/packages/in_app_purchase/in_app_purchase/example/android/build.gradle +++ b/packages/in_app_purchase/in_app_purchase/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.0.1' } } diff --git a/packages/in_app_purchase/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/in_app_purchase/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties index bc6a58afdda2..b8793d3c0d69 100644 --- a/packages/in_app_purchase/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/in_app_purchase/in_app_purchase/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-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip 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 651652b40c3a..9e53b4bf8b8e 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 @@ -4,12 +4,14 @@ import 'dart:async'; import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; + import 'consumable_store.dart'; void main() { @@ -18,7 +20,9 @@ void main() { runApp(_MyApp()); } -const bool _kAutoConsume = true; +// Auto-consume must be true on iOS. +// To try without auto-consume on another platform, change `true` to `false` here. +final bool _kAutoConsume = Platform.isIOS || true; const String _kConsumableId = 'consumable'; const String _kUpgradeId = 'upgrade'; @@ -33,7 +37,7 @@ const List _kProductIds = [ class _MyApp extends StatefulWidget { @override - _MyAppState createState() => _MyAppState(); + State<_MyApp> createState() => _MyAppState(); } class _MyAppState extends State<_MyApp> { @@ -192,9 +196,11 @@ class _MyAppState extends State<_MyApp> { } final Widget storeHeader = ListTile( leading: Icon(_isAvailable ? Icons.check : Icons.block, - color: _isAvailable ? Colors.green : ThemeData.light().errorColor), - title: Text( - 'The store is ' + (_isAvailable ? 'available' : 'unavailable') + '.'), + color: _isAvailable + ? Colors.green + : ThemeData.light().colorScheme.error), + title: + Text('The store is ${_isAvailable ? 'available' : 'unavailable'}.'), ); final List children = [storeHeader]; @@ -203,7 +209,7 @@ class _MyAppState extends State<_MyApp> { const Divider(), ListTile( title: Text('Not connected', - style: TextStyle(color: ThemeData.light().errorColor)), + style: TextStyle(color: ThemeData.light().colorScheme.error)), subtitle: const Text( 'Unable to connect to the payments processor. Has this app been configured correctly? See the example README for instructions.'), ), @@ -227,7 +233,7 @@ class _MyAppState extends State<_MyApp> { if (_notFoundIds.isNotEmpty) { productList.add(ListTile( title: Text('[${_notFoundIds.join(", ")}] not found', - style: TextStyle(color: ThemeData.light().errorColor)), + style: TextStyle(color: ThemeData.light().colorScheme.error)), subtitle: const Text( 'This app needs special configuration to run. Please see example/README.md for instructions.'))); } @@ -247,60 +253,61 @@ class _MyAppState extends State<_MyApp> { (ProductDetails productDetails) { final PurchaseDetails? previousPurchase = purchases[productDetails.id]; return ListTile( - title: Text( - productDetails.title, - ), - subtitle: Text( - productDetails.description, - ), - trailing: previousPurchase != null - ? IconButton( - onPressed: () => confirmPriceChange(context), - icon: const Icon(Icons.upgrade)) - : TextButton( - child: Text(productDetails.price), - style: TextButton.styleFrom( - backgroundColor: Colors.green[800], - primary: Colors.white, - ), - onPressed: () { - late PurchaseParam purchaseParam; - - if (Platform.isAndroid) { - // NOTE: If you are making a subscription purchase/upgrade/downgrade, we recommend you to - // verify the latest status of you your subscription by using server side receipt validation - // and update the UI accordingly. The subscription purchase status shown - // inside the app may not be accurate. - final GooglePlayPurchaseDetails? oldSubscription = - _getOldSubscription(productDetails, purchases); - - purchaseParam = GooglePlayPurchaseParam( - productDetails: productDetails, - applicationUserName: null, - changeSubscriptionParam: (oldSubscription != null) - ? ChangeSubscriptionParam( - oldPurchaseDetails: oldSubscription, - prorationMode: ProrationMode - .immediateWithTimeProration, - ) - : null); - } else { - purchaseParam = PurchaseParam( + title: Text( + productDetails.title, + ), + subtitle: Text( + productDetails.description, + ), + trailing: previousPurchase != null + ? IconButton( + onPressed: () => confirmPriceChange(context), + icon: const Icon(Icons.upgrade)) + : TextButton( + style: TextButton.styleFrom( + backgroundColor: Colors.green[800], + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + primary: Colors.white, + ), + onPressed: () { + late PurchaseParam purchaseParam; + + if (Platform.isAndroid) { + // NOTE: If you are making a subscription purchase/upgrade/downgrade, we recommend you to + // verify the latest status of you your subscription by using server side receipt validation + // and update the UI accordingly. The subscription purchase status shown + // inside the app may not be accurate. + final GooglePlayPurchaseDetails? oldSubscription = + _getOldSubscription(productDetails, purchases); + + purchaseParam = GooglePlayPurchaseParam( productDetails: productDetails, - applicationUserName: null, - ); - } - - if (productDetails.id == _kConsumableId) { - _inAppPurchase.buyConsumable( - purchaseParam: purchaseParam, - autoConsume: _kAutoConsume || Platform.isIOS); - } else { - _inAppPurchase.buyNonConsumable( - purchaseParam: purchaseParam); - } - }, - )); + changeSubscriptionParam: (oldSubscription != null) + ? ChangeSubscriptionParam( + oldPurchaseDetails: oldSubscription, + prorationMode: + ProrationMode.immediateWithTimeProration, + ) + : null); + } else { + purchaseParam = PurchaseParam( + productDetails: productDetails, + ); + } + + if (productDetails.id == _kConsumableId) { + _inAppPurchase.buyConsumable( + purchaseParam: purchaseParam, + autoConsume: _kAutoConsume); + } else { + _inAppPurchase.buyNonConsumable( + purchaseParam: purchaseParam); + } + }, + child: Text(productDetails.price), + ), + ); }, )); @@ -340,9 +347,9 @@ class _MyAppState extends State<_MyApp> { const Divider(), GridView.count( crossAxisCount: 5, - children: tokens, shrinkWrap: true, padding: const EdgeInsets.all(16.0), + children: tokens, ) ])); } @@ -355,16 +362,17 @@ class _MyAppState extends State<_MyApp> { return Padding( padding: const EdgeInsets.all(4.0), child: Row( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - child: const Text('Restore purchases'), style: TextButton.styleFrom( backgroundColor: Theme.of(context).primaryColor, + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use primary: Colors.white, ), onPressed: () => _inAppPurchase.restorePurchases(), + child: const Text('Restore purchases'), ), ], ), 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 4a79b190bff9..74ad9aafb768 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: ">=1.20.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -16,12 +16,15 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ + in_app_purchase_android: ^0.2.1 + in_app_purchase_storekit: ^0.3.0+1 shared_preferences: ^2.0.0 dev_dependencies: flutter_driver: sdk: flutter - + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart b/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart index df05d8ce86c3..d550e48ebc3a 100644 --- a/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart +++ b/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index 139d58318780..40b0ea9a152b 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.0.1 +version: 3.0.8 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - in_app_purchase_android: ^0.2.1 + in_app_purchase_android: ^0.2.3 in_app_purchase_platform_interface: ^1.0.0 in_app_purchase_storekit: ^0.3.0+1 diff --git a/packages/in_app_purchase/in_app_purchase/test/in_app_purchase_test.dart b/packages/in_app_purchase/in_app_purchase/test/in_app_purchase_test.dart index 644d26ed50ad..58f7398add16 100644 --- a/packages/in_app_purchase/in_app_purchase/test/in_app_purchase_test.dart +++ b/packages/in_app_purchase/in_app_purchase/test/in_app_purchase_test.dart @@ -184,12 +184,12 @@ class MockInAppPurchasePlatform extends Fake @override Future completePurchase(PurchaseDetails purchase) { log.add(const MethodCall('completePurchase')); - return Future.value(null); + return Future.value(); } @override Future restorePurchases({String? applicationUserName}) { log.add(const MethodCall('restorePurchases')); - return Future.value(null); + return Future.value(); } } 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 a871cfafb4a2..b595d7e148d9 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,61 @@ +## 0.2.3+6 + +* Updates android gradle plugin to 7.3.1. + +## 0.2.3+5 + +* Updates imports for `prefer_relative_imports`. + +## 0.2.3+4 + +* Updates minimum Flutter version to 2.10. +* Adds IMMEDIATE_AND_CHARGE_FULL_PRICE to the `ProrationMode`. + +## 0.2.3+3 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 0.2.3+2 + +* Fixes incorrect json key in `queryPurchasesAsync` that fixes restore purchases functionality. + +## 0.2.3+1 + +* Updates `json_serializable` to fix warnings in generated code. + +## 0.2.3 + +* Upgrades Google Play Billing Library to 5.0 +* Migrates APIs to support breaking changes in new Google Play Billing API +* `PurchaseWrapper` and `PurchaseHistoryRecordWrapper` now handles `skus` a list of sku strings. `sku` is deprecated. + +## 0.2.2+8 + +* Ignores deprecation warnings for upcoming styleFrom button API changes. + +## 0.2.2+7 + +* Updates references to the obsolete master branch. + +## 0.2.2+6 + +* Enables mocking models by changing overridden operator == parameter type from `dynamic` to `Object`. + +## 0.2.2+5 + +* Minor fixes for new analysis options. + +## 0.2.2+4 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.2.2+3 + +* Migrates from `ui.hash*` to `Object.hash*`. +* Updates minimum Flutter version to 2.5.0. + ## 0.2.2+2 * Internal code cleanup for stricter analysis options. diff --git a/packages/in_app_purchase/in_app_purchase_android/README.md b/packages/in_app_purchase/in_app_purchase_android/README.md index d64fbfb8c49a..423c07577ca4 100644 --- a/packages/in_app_purchase/in_app_purchase_android/README.md +++ b/packages/in_app_purchase/in_app_purchase_android/README.md @@ -21,7 +21,7 @@ editing any of the serialized data structs, rebuild the serializers by running watch the filesystem for changes. If you would like to contribute to the plugin, check out our -[contribution guide](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md). +[contribution guide](https://github.com/flutter/plugins/blob/main/CONTRIBUTING.md). [1]: https://pub.dev/packages/in_app_purchase 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 cbb0855509a5..704ab36c253b 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 @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.3.1' } } @@ -29,6 +29,8 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { + // TODO(stuartmorgan): Enable when gradle is updated. + // disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' } @@ -52,11 +54,11 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.0' - implementation 'com.android.billingclient:billing:3.0.2' - testImplementation 'junit:junit:4.12' - testImplementation 'org.json:json:20180813' - testImplementation 'org.mockito:mockito-core:3.6.0' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + implementation 'androidx.annotation:annotation:1.3.0' + implementation 'com.android.billingclient:billing:5.0.0' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.json:json:20220924' + testImplementation 'org.mockito:mockito-core:4.7.0' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java index b21ab6992608..6f4e4bbfd8ee 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java @@ -39,6 +39,7 @@ static final class MethodNames { static final String ON_PURCHASES_UPDATED = "PurchasesUpdatedListener#onPurchasesUpdated(int, List)"; static final String QUERY_PURCHASES = "BillingClient#queryPurchases(String)"; + static final String QUERY_PURCHASES_ASYNC = "BillingClient#queryPurchasesAsync(String)"; static final String QUERY_PURCHASE_HISTORY_ASYNC = "BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)"; static final String CONSUME_PURCHASE_ASYNC = @@ -48,6 +49,7 @@ static final class MethodNames { static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)"; static final String LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW = "BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)"; + static final String GET_CONNECTION_STATE = "BillingClient#getConnectionState()"; private MethodNames() {}; } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index 23b9cb6ecda2..bdf52dc40e55 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -5,7 +5,7 @@ package io.flutter.plugins.inapppurchase; import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; -import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesResult; +import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList; import android.app.Activity; @@ -25,8 +25,12 @@ import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.PriceChangeFlowParams; +import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchaseHistoryRecord; import com.android.billingclient.api.PurchaseHistoryResponseListener; +import com.android.billingclient.api.PurchasesResponseListener; +import com.android.billingclient.api.QueryPurchaseHistoryParams; +import com.android.billingclient.api.QueryPurchasesParams; import com.android.billingclient.api.SkuDetails; import com.android.billingclient.api.SkuDetailsParams; import com.android.billingclient.api.SkuDetailsResponseListener; @@ -42,7 +46,7 @@ class MethodCallHandlerImpl private static final String TAG = "InAppPurchasePlugin"; private static final String LOAD_SKU_DOC_URL = - "https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/in_app_purchase/README.md#loading-products-for-sale"; + "https://github.com/flutter/plugins/blob/main/packages/in_app_purchase/in_app_purchase/README.md#loading-products-for-sale"; @Nullable private BillingClient billingClient; private final BillingClientFactory billingClientFactory; @@ -131,10 +135,14 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { : ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY, result); break; - case InAppPurchasePlugin.MethodNames.QUERY_PURCHASES: - queryPurchases((String) call.argument("skuType"), result); + case InAppPurchasePlugin.MethodNames.QUERY_PURCHASES: // Legacy method name. + queryPurchasesAsync((String) call.argument("skuType"), result); + break; + case InAppPurchasePlugin.MethodNames.QUERY_PURCHASES_ASYNC: + queryPurchasesAsync((String) call.argument("skuType"), result); break; case InAppPurchasePlugin.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC: + Log.e("flutter", (String) call.argument("skuType")); queryPurchaseHistoryAsync((String) call.argument("skuType"), result); break; case InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC: @@ -149,6 +157,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { case InAppPurchasePlugin.MethodNames.LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW: launchPriceChangeConfirmationFlow((String) call.argument("sku"), result); break; + case InAppPurchasePlugin.MethodNames.GET_CONNECTION_STATE: + getConnectionState(result); + break; default: result.notImplemented(); } @@ -174,6 +185,7 @@ private void isReady(MethodChannel.Result result) { result.success(billingClient.isReady()); } + // TODO(garyq): Migrate to new subscriptions API: https://developer.android.com/google/play/billing/migrate-gpblv5 private void querySkuDetailsAsync( final String skuType, final List skusList, final MethodChannel.Result result) { if (billingClientError(result)) { @@ -208,7 +220,6 @@ private void launchBillingFlow( if (billingClientError(result)) { return; } - SkuDetails skuDetails = cachedSkus.get(sku); if (skuDetails == null) { result.error( @@ -255,12 +266,15 @@ private void launchBillingFlow( if (obfuscatedProfileId != null && !obfuscatedProfileId.isEmpty()) { paramsBuilder.setObfuscatedProfileId(obfuscatedProfileId); } - if (oldSku != null && !oldSku.isEmpty()) { - paramsBuilder.setOldSku(oldSku, purchaseToken); + BillingFlowParams.SubscriptionUpdateParams.Builder subscriptionUpdateParamsBuilder = + BillingFlowParams.SubscriptionUpdateParams.newBuilder(); + if (oldSku != null && !oldSku.isEmpty() && purchaseToken != null) { + subscriptionUpdateParamsBuilder.setOldPurchaseToken(purchaseToken); + // The proration mode value has to match one of the following declared in + // https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode + subscriptionUpdateParamsBuilder.setReplaceProrationMode(prorationMode); + paramsBuilder.setSubscriptionUpdateParams(subscriptionUpdateParamsBuilder.build()); } - // The proration mode value has to match one of the following declared in - // https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode - paramsBuilder.setReplaceSkusProrationMode(prorationMode); result.success( Translator.fromBillingResult( billingClient.launchBillingFlow(activity, paramsBuilder.build()))); @@ -286,14 +300,30 @@ public void onConsumeResponse(BillingResult billingResult, String outToken) { billingClient.consumeAsync(params, listener); } - private void queryPurchases(String skuType, MethodChannel.Result result) { + private void queryPurchasesAsync(String skuType, MethodChannel.Result result) { if (billingClientError(result)) { return; } // Like in our connect call, consider the billing client responding a "success" here regardless // of status code. - result.success(fromPurchasesResult(billingClient.queryPurchases(skuType))); + QueryPurchasesParams.Builder paramsBuilder = QueryPurchasesParams.newBuilder(); + paramsBuilder.setProductType(skuType); + billingClient.queryPurchasesAsync( + paramsBuilder.build(), + new PurchasesResponseListener() { + @Override + public void onQueryPurchasesResponse( + BillingResult billingResult, List purchasesList) { + final Map serialized = new HashMap<>(); + // The response code is no longer passed, as part of billing 4.0, so we pass OK here + // as success is implied by calling this callback. + serialized.put("responseCode", BillingClient.BillingResponseCode.OK); + serialized.put("billingResult", Translator.fromBillingResult(billingResult)); + serialized.put("purchasesList", fromPurchasesList(purchasesList)); + result.success(serialized); + } + }); } private void queryPurchaseHistoryAsync(String skuType, final MethodChannel.Result result) { @@ -302,7 +332,7 @@ private void queryPurchaseHistoryAsync(String skuType, final MethodChannel.Resul } billingClient.queryPurchaseHistoryAsync( - skuType, + QueryPurchaseHistoryParams.newBuilder().setProductType(skuType).build(), new PurchaseHistoryResponseListener() { @Override public void onPurchaseHistoryResponse( @@ -316,6 +346,15 @@ public void onPurchaseHistoryResponse( }); } + private void getConnectionState(final MethodChannel.Result result) { + if (billingClientError(result)) { + return; + } + final Map serialized = new HashMap<>(); + serialized.put("connectionState", billingClient.getConnectionState()); + result.success(serialized); + } + private void startConnection(final int handle, final MethodChannel.Result result) { if (billingClient == null) { billingClient = billingClientFactory.createBillingClient(applicationContext, methodChannel); diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java index 7546fe7db58d..5a0cf6ea3727 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java @@ -8,7 +8,6 @@ import com.android.billingclient.api.AccountIdentifiers; import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.Purchase; -import com.android.billingclient.api.Purchase.PurchasesResult; import com.android.billingclient.api.PurchaseHistoryRecord; import com.android.billingclient.api.SkuDetails; import java.util.ArrayList; @@ -56,17 +55,19 @@ static List> fromSkuDetailsList( static HashMap fromPurchase(Purchase purchase) { HashMap info = new HashMap<>(); + List skus = purchase.getSkus(); info.put("orderId", purchase.getOrderId()); info.put("packageName", purchase.getPackageName()); info.put("purchaseTime", purchase.getPurchaseTime()); info.put("purchaseToken", purchase.getPurchaseToken()); info.put("signature", purchase.getSignature()); - info.put("sku", purchase.getSku()); + info.put("skus", skus); info.put("isAutoRenewing", purchase.isAutoRenewing()); info.put("originalJson", purchase.getOriginalJson()); info.put("developerPayload", purchase.getDeveloperPayload()); info.put("isAcknowledged", purchase.isAcknowledged()); info.put("purchaseState", purchase.getPurchaseState()); + info.put("quantity", purchase.getQuantity()); AccountIdentifiers accountIdentifiers = purchase.getAccountIdentifiers(); if (accountIdentifiers != null) { info.put("obfuscatedAccountId", accountIdentifiers.getObfuscatedAccountId()); @@ -78,12 +79,14 @@ static HashMap fromPurchase(Purchase purchase) { static HashMap fromPurchaseHistoryRecord( PurchaseHistoryRecord purchaseHistoryRecord) { HashMap info = new HashMap<>(); + List skus = purchaseHistoryRecord.getSkus(); info.put("purchaseTime", purchaseHistoryRecord.getPurchaseTime()); info.put("purchaseToken", purchaseHistoryRecord.getPurchaseToken()); info.put("signature", purchaseHistoryRecord.getSignature()); - info.put("sku", purchaseHistoryRecord.getSku()); + info.put("skus", skus); info.put("developerPayload", purchaseHistoryRecord.getDeveloperPayload()); info.put("originalJson", purchaseHistoryRecord.getOriginalJson()); + info.put("quantity", purchaseHistoryRecord.getQuantity()); return info; } @@ -112,14 +115,6 @@ static List> fromPurchaseHistoryRecordList( return serialized; } - static HashMap fromPurchasesResult(PurchasesResult purchasesResult) { - HashMap info = new HashMap<>(); - info.put("responseCode", purchasesResult.getResponseCode()); - info.put("billingResult", fromBillingResult(purchasesResult.getBillingResult())); - info.put("purchasesList", fromPurchasesList(purchasesResult.getPurchasesList())); - return info; - } - static HashMap fromBillingResult(BillingResult billingResult) { HashMap info = new HashMap<>(); info.put("responseCode", billingResult.getResponseCode()); diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index d676bf3436ee..ffebb2544a13 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -13,26 +13,25 @@ import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_DISCONNECT; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_PURCHASES_UPDATED; -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASES; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASES_ASYNC; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_SKU_DETAILS; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.START_CONNECTION; import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; -import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesResult; import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Collections.unmodifiableList; import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -56,9 +55,11 @@ import com.android.billingclient.api.PriceChangeConfirmationListener; import com.android.billingclient.api.PriceChangeFlowParams; import com.android.billingclient.api.Purchase; -import com.android.billingclient.api.Purchase.PurchasesResult; import com.android.billingclient.api.PurchaseHistoryRecord; import com.android.billingclient.api.PurchaseHistoryResponseListener; +import com.android.billingclient.api.PurchasesResponseListener; +import com.android.billingclient.api.QueryPurchaseHistoryParams; +import com.android.billingclient.api.QueryPurchasesParams; import com.android.billingclient.api.SkuDetails; import com.android.billingclient.api.SkuDetailsParams; import com.android.billingclient.api.SkuDetailsResponseListener; @@ -66,9 +67,12 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.json.JSONException; import org.junit.Before; import org.junit.Test; @@ -76,6 +80,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; public class MethodCallHandlerTest { private MethodCallHandlerImpl methodChannelHandler; @@ -294,7 +300,6 @@ public void launchBillingFlow_null_AccountId_do_not_crash() { ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); BillingFlowParams params = billingFlowParamsCaptor.getValue(); - assertEquals(params.getSku(), skuId); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); @@ -327,8 +332,6 @@ public void launchBillingFlow_ok_null_OldSku() { ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); BillingFlowParams params = billingFlowParamsCaptor.getValue(); - assertEquals(params.getSku(), skuId); - assertNull(params.getOldSku()); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); verify(result, times(1)).success(fromBillingResult(billingResult)); @@ -380,8 +383,6 @@ public void launchBillingFlow_ok_oldSku() { ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); BillingFlowParams params = billingFlowParamsCaptor.getValue(); - assertEquals(params.getSku(), skuId); - assertEquals(params.getOldSku(), oldSkuId); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); @@ -413,7 +414,6 @@ public void launchBillingFlow_ok_AccountId() { ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); BillingFlowParams params = billingFlowParamsCaptor.getValue(); - assertEquals(params.getSku(), skuId); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); @@ -451,10 +451,6 @@ public void launchBillingFlow_ok_Proration() { ArgumentCaptor.forClass(BillingFlowParams.class); verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); BillingFlowParams params = billingFlowParamsCaptor.getValue(); - assertEquals(params.getSku(), skuId); - assertEquals(params.getOldSku(), oldSkuId); - assertEquals(params.getOldSkuPurchaseToken(), purchaseToken); - assertEquals(params.getReplaceSkusProrationMode(), prorationMode); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); @@ -495,6 +491,43 @@ public void launchBillingFlow_ok_Proration_with_null_OldSku() { verify(result, never()).success(any()); } + @Test + public void launchBillingFlow_ok_Full() { + // Fetch the sku details first and query the method call + String skuId = "foo"; + String oldSkuId = "oldFoo"; + String purchaseToken = "purchaseTokenFoo"; + String accountId = "account"; + int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE; + queryForSkus(unmodifiableList(asList(skuId, oldSkuId))); + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + arguments.put("oldSku", oldSkuId); + arguments.put("purchaseToken", purchaseToken); + arguments.put("prorationMode", prorationMode); + MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + + // Launch the billing flow + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); + methodChannelHandler.onMethodCall(launchCall, result); + + // Verify we pass the arguments to the billing flow + ArgumentCaptor billingFlowParamsCaptor = + ArgumentCaptor.forClass(BillingFlowParams.class); + verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); + BillingFlowParams params = billingFlowParamsCaptor.getValue(); + + // Verify we pass the response code to result + verify(result, never()).error(any(), any(), any()); + verify(result, times(1)).success(fromBillingResult(billingResult)); + } + @Test public void launchBillingFlow_clientDisconnected() { // Prepare the launch call after disconnecting the client @@ -554,42 +587,69 @@ public void launchBillingFlow_oldSkuNotFound() { } @Test - public void queryPurchases() { - establishConnectedBillingClient(null, null); - PurchasesResult purchasesResult = mock(PurchasesResult.class); - Purchase purchase = buildPurchase("foo"); - when(purchasesResult.getPurchasesList()).thenReturn(asList(purchase)); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - when(purchasesResult.getBillingResult()).thenReturn(billingResult); - when(mockBillingClient.queryPurchases(SkuType.INAPP)).thenReturn(purchasesResult); + public void queryPurchases_clientDisconnected() { + // Prepare the launch call after disconnecting the client + methodChannelHandler.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class)); HashMap arguments = new HashMap<>(); arguments.put("skuType", SkuType.INAPP); - methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES, arguments), result); + methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES_ASYNC, arguments), result); - // Verify we pass the response to result - ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); - verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(resultCaptor.capture()); - assertEquals(fromPurchasesResult(purchasesResult), resultCaptor.getValue()); + // Assert that we sent an error back. + verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); + verify(result, never()).success(any()); } @Test - public void queryPurchases_clientDisconnected() { - // Prepare the launch call after disconnecting the client - methodChannelHandler.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class)); + public void queryPurchases_returns_success() throws Exception { + establishConnectedBillingClient(null, null); + + CountDownLatch lock = new CountDownLatch(1); + doAnswer( + new Answer() { + public Object answer(InvocationOnMock invocation) { + lock.countDown(); + return null; + } + }) + .when(result) + .success(any(HashMap.class)); + + ArgumentCaptor purchasesResponseListenerArgumentCaptor = + ArgumentCaptor.forClass(PurchasesResponseListener.class); + doAnswer( + new Answer() { + public Object answer(InvocationOnMock invocation) { + BillingResult.Builder resultBuilder = + BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.OK) + .setDebugMessage("hello message"); + purchasesResponseListenerArgumentCaptor + .getValue() + .onQueryPurchasesResponse(resultBuilder.build(), new ArrayList()); + return null; + } + }) + .when(mockBillingClient) + .queryPurchasesAsync( + any(QueryPurchasesParams.class), purchasesResponseListenerArgumentCaptor.capture()); HashMap arguments = new HashMap<>(); arguments.put("skuType", SkuType.INAPP); - methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES, arguments), result); + methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES_ASYNC, arguments), result); - // Assert that we sent an error back. - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); - verify(result, never()).success(any()); + lock.await(5000, TimeUnit.MILLISECONDS); + + verify(result, never()).error(any(), any(), any()); + + ArgumentCaptor hashMapCaptor = ArgumentCaptor.forClass(HashMap.class); + verify(result, times(1)).success(hashMapCaptor.capture()); + + HashMap map = hashMapCaptor.getValue(); + assert (map.containsKey("responseCode")); + assert (map.containsKey("billingResult")); + assert (map.containsKey("purchasesList")); + assert ((int) map.get("responseCode") == 0); } @Test @@ -613,7 +673,7 @@ public void queryPurchaseHistoryAsync() { // Verify we pass the data to result verify(mockBillingClient) - .queryPurchaseHistoryAsync(eq(SkuType.INAPP), listenerCaptor.capture()); + .queryPurchaseHistoryAsync(any(QueryPurchaseHistoryParams.class), listenerCaptor.capture()); listenerCaptor.getValue().onPurchaseHistoryResponse(billingResult, purchasesList); verify(result).success(resultCaptor.capture()); HashMap resultData = resultCaptor.getValue(); diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java index 2837dceea652..79852e7e8ca5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java @@ -9,15 +9,12 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import androidx.annotation.NonNull; import com.android.billingclient.api.AccountIdentifiers; import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.Purchase; -import com.android.billingclient.api.Purchase.PurchasesResult; import com.android.billingclient.api.PurchaseHistoryRecord; import com.android.billingclient.api.SkuDetails; import java.util.Arrays; @@ -138,37 +135,6 @@ public void fromPurchasesList_null() { assertEquals(Collections.emptyList(), Translator.fromPurchasesList(null)); } - @Test - public void fromPurchasesResult() throws JSONException { - PurchasesResult result = mock(PurchasesResult.class); - final String purchase2Json = - "{\"orderId\":\"foo2\",\"packageName\":\"bar\",\"productId\":\"consumable\",\"purchaseTime\":11111111,\"purchaseState\":0,\"purchaseToken\":\"baz\",\"developerPayload\":\"dummy payload\",\"isAcknowledged\":\"true\"}"; - final String signature = "signature"; - final List expectedPurchases = - Arrays.asList( - new Purchase(PURCHASE_EXAMPLE_JSON, signature), new Purchase(purchase2Json, signature)); - when(result.getPurchasesList()).thenReturn(expectedPurchases); - when(result.getResponseCode()).thenReturn(BillingClient.BillingResponseCode.OK); - BillingResult newBillingResult = - BillingResult.newBuilder() - .setDebugMessage("dummy debug message") - .setResponseCode(BillingClient.BillingResponseCode.OK) - .build(); - when(result.getBillingResult()).thenReturn(newBillingResult); - final HashMap serialized = Translator.fromPurchasesResult(result); - - assertEquals(BillingClient.BillingResponseCode.OK, serialized.get("responseCode")); - List> serializedPurchases = - (List>) serialized.get("purchasesList"); - assertEquals(expectedPurchases.size(), serializedPurchases.size()); - assertSerialized(expectedPurchases.get(0), serializedPurchases.get(0)); - assertSerialized(expectedPurchases.get(1), serializedPurchases.get(1)); - - Map billingResultMap = (Map) serialized.get("billingResult"); - assertEquals(billingResultMap.get("responseCode"), newBillingResult.getResponseCode()); - assertEquals(billingResultMap.get("debugMessage"), newBillingResult.getDebugMessage()); - } - @Test public void fromBillingResult() throws JSONException { BillingResult newBillingResult = @@ -232,7 +198,7 @@ private void assertSerialized(Purchase expected, Map serialized) assertEquals(expected.getPurchaseToken(), serialized.get("purchaseToken")); assertEquals(expected.getSignature(), serialized.get("signature")); assertEquals(expected.getOriginalJson(), serialized.get("originalJson")); - assertEquals(expected.getSku(), serialized.get("sku")); + assertEquals(expected.getSkus(), serialized.get("skus")); assertEquals(expected.getDeveloperPayload(), serialized.get("developerPayload")); assertEquals(expected.isAcknowledged(), serialized.get("isAcknowledged")); assertEquals(expected.getPurchaseState(), serialized.get("purchaseState")); @@ -251,7 +217,7 @@ private void assertSerialized(PurchaseHistoryRecord expected, Map _kProductIds = [ class _MyApp extends StatefulWidget { @override - _MyAppState createState() => _MyAppState(); + State<_MyApp> createState() => _MyAppState(); } class _MyAppState extends State<_MyApp> { @@ -186,9 +186,11 @@ class _MyAppState extends State<_MyApp> { } final Widget storeHeader = ListTile( leading: Icon(_isAvailable ? Icons.check : Icons.block, - color: _isAvailable ? Colors.green : ThemeData.light().errorColor), - title: Text( - 'The store is ' + (_isAvailable ? 'available' : 'unavailable') + '.'), + color: _isAvailable + ? Colors.green + : ThemeData.light().colorScheme.error), + title: + Text('The store is ${_isAvailable ? 'available' : 'unavailable'}.'), ); final List children = [storeHeader]; @@ -197,7 +199,7 @@ class _MyAppState extends State<_MyApp> { const Divider(), ListTile( title: Text('Not connected', - style: TextStyle(color: ThemeData.light().errorColor)), + style: TextStyle(color: ThemeData.light().colorScheme.error)), subtitle: const Text( 'Unable to connect to the payments processor. Has this app been configured correctly? See the example README for instructions.'), ), @@ -221,7 +223,7 @@ class _MyAppState extends State<_MyApp> { if (_notFoundIds.isNotEmpty) { productList.add(ListTile( title: Text('[${_notFoundIds.join(", ")}] not found', - style: TextStyle(color: ThemeData.light().errorColor)), + style: TextStyle(color: ThemeData.light().colorScheme.error)), subtitle: const Text( 'This app needs special configuration to run. Please see example/README.md for instructions.'))); } @@ -264,9 +266,10 @@ class _MyAppState extends State<_MyApp> { }, icon: const Icon(Icons.upgrade)) : TextButton( - child: Text(productDetails.price), style: TextButton.styleFrom( backgroundColor: Colors.green[800], + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use primary: Colors.white, ), onPressed: () { @@ -281,7 +284,6 @@ class _MyAppState extends State<_MyApp> { final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( productDetails: productDetails, - applicationUserName: null, changeSubscriptionParam: oldSubscription != null ? ChangeSubscriptionParam( oldPurchaseDetails: oldSubscription, @@ -291,12 +293,14 @@ class _MyAppState extends State<_MyApp> { if (productDetails.id == _kConsumableId) { _inAppPurchasePlatform.buyConsumable( purchaseParam: purchaseParam, - autoConsume: _kAutoConsume || Platform.isIOS); + // ignore: avoid_redundant_argument_values + autoConsume: _kAutoConsume); } else { _inAppPurchasePlatform.buyNonConsumable( purchaseParam: purchaseParam); } }, + child: Text(productDetails.price), )); }, )); @@ -337,9 +341,9 @@ class _MyAppState extends State<_MyApp> { const Divider(), GridView.count( crossAxisCount: 5, - children: tokens, shrinkWrap: true, padding: const EdgeInsets.all(16.0), + children: tokens, ) ])); } 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 9c16efc66e95..af760a3ada46 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: ">=1.9.1+hotfix.2" + flutter: ">=2.10.0" dependencies: flutter: @@ -22,6 +22,8 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter 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 416eb5680770..b64eaab49a9d 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 @@ -10,8 +10,6 @@ import 'package:json_annotation/json_annotation.dart'; import '../../billing_client_wrappers.dart'; import '../channel.dart'; -import 'purchase_wrapper.dart'; -import 'sku_details_wrapper.dart'; part 'billing_client_wrapper.g.dart'; @@ -86,7 +84,9 @@ class BillingClient { /// **Deprecation warning:** it is no longer required to call /// [enablePendingPurchases] when initializing your application. @Deprecated( - 'The requirement to call `enablePendingPurchases()` has become obsolete since Google Play no longer accepts app submissions that don\'t support pending purchases.') + 'The requirement to call `enablePendingPurchases()` has become obsolete ' + "since Google Play no longer accepts app submissions that don't support " + 'pending purchases.') void enablePendingPurchases() { // No-op, until it is time to completely remove this method from the API. } @@ -124,7 +124,7 @@ class BillingClient { /// /// This triggers the destruction of the `BillingClient` instance in Java. Future endConnection() async { - return channel.invokeMethod('BillingClient#endConnection()', null); + return channel.invokeMethod('BillingClient#endConnection()'); } /// Returns a list of [SkuDetailsWrapper]s that have [SkuDetailsWrapper.sku] @@ -495,7 +495,8 @@ enum ProrationMode { @JsonValue(0) unknownSubscriptionUpgradeDowngradePolicy, - /// Replacement takes effect immediately, and the remaining time will be prorated and credited to the user. + /// Replacement takes effect immediately, and the remaining time will be prorated + /// and credited to the user. /// /// This is the current default behavior. @JsonValue(1) @@ -508,15 +509,23 @@ enum ProrationMode { @JsonValue(2) immediateAndChargeProratedPrice, - /// Replacement takes effect immediately, and the new price will be charged on next recurrence time. + /// Replacement takes effect immediately, and the new price will be charged on next + /// recurrence time. /// /// The billing cycle stays the same. @JsonValue(3) immediateWithoutProration, - /// Replacement takes effect when the old plan expires, and the new price will be charged at the same time. + /// Replacement takes effect when the old plan expires, and the new price will + /// be charged at the same time. @JsonValue(4) deferred, + + /// Replacement takes effect immediately, and the user is charged full price + /// of new plan and is given a full billing cycle of subscription, plus + /// remaining prorated time from the old plan. + @JsonValue(5) + immediateAndChargeFullPrice, } /// Serializer for [ProrationMode]. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart index efe7656d2138..99355a1b91fb 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart @@ -32,6 +32,7 @@ const _$ProrationModeEnumMap = { ProrationMode.immediateAndChargeProratedPrice: 2, ProrationMode.immediateWithoutProration: 3, ProrationMode.deferred: 4, + ProrationMode.immediateAndChargeFullPrice: 5, }; const _$BillingClientFeatureEnumMap = { 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 653e5147f9b0..4e6b953096e2 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 @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -35,7 +33,8 @@ class PurchaseWrapper { required this.purchaseTime, required this.purchaseToken, required this.signature, - required this.sku, + @Deprecated('Use skus instead') String? sku, + required this.skus, required this.isAutoRenewing, required this.originalJson, this.developerPayload, @@ -43,7 +42,7 @@ class PurchaseWrapper { required this.purchaseState, this.obfuscatedAccountId, this.obfuscatedProfileId, - }); + }) : _sku = sku; /// Factory for creating a [PurchaseWrapper] from a [Map] with the purchase details. factory PurchaseWrapper.fromJson(Map map) => @@ -71,7 +70,7 @@ class PurchaseWrapper { } @override - int get hashCode => hashValues( + int get hashCode => Object.hash( orderId, packageName, purchaseTime, @@ -106,8 +105,14 @@ class PurchaseWrapper { final String signature; /// The product ID of this purchase. - @JsonKey(defaultValue: '') - final String sku; + @Deprecated('Use skus instead') + @JsonKey(ignore: true) + String get sku => _sku ?? (skus.isNotEmpty ? skus.first : ''); + final String? _sku; + + /// The product IDs of this purchase. + @JsonKey(defaultValue: []) + final List skus; /// True for subscriptions that renew automatically. Does not apply to /// [SkuType.inapp] products. @@ -180,10 +185,11 @@ class PurchaseHistoryRecordWrapper { required this.purchaseTime, required this.purchaseToken, required this.signature, - required this.sku, + @Deprecated('Use skus instead') String? sku, + required this.skus, required this.originalJson, required this.developerPayload, - }); + }) : _sku = sku; /// Factory for creating a [PurchaseHistoryRecordWrapper] from a [Map] with the record details. factory PurchaseHistoryRecordWrapper.fromJson(Map map) => @@ -203,8 +209,15 @@ class PurchaseHistoryRecordWrapper { final String signature; /// The product ID of this purchase. - @JsonKey(defaultValue: '') - final String sku; + @Deprecated('Use skus instead') + @JsonKey(ignore: true) + String get sku => _sku ?? (skus.isNotEmpty ? skus.first : ''); + + final String? _sku; + + /// The product ID of this purchase. + @JsonKey(defaultValue: []) + final List skus; /// Details about this purchase, in JSON. /// @@ -238,7 +251,7 @@ class PurchaseHistoryRecordWrapper { } @override - int get hashCode => hashValues(purchaseTime, purchaseToken, signature, sku, + int get hashCode => Object.hash(purchaseTime, purchaseToken, signature, sku, originalJson, developerPayload); } @@ -278,7 +291,7 @@ class PurchasesResultWrapper { } @override - int get hashCode => hashValues(billingResult, responseCode, purchasesList); + int get hashCode => Object.hash(billingResult, responseCode, purchasesList); /// The detailed description of the status of the operation. final BillingResultWrapper billingResult; @@ -326,7 +339,7 @@ class PurchasesHistoryResult { } @override - int get hashCode => hashValues(billingResult, purchaseHistoryRecordList); + int get hashCode => Object.hash(billingResult, purchaseHistoryRecordList); /// The detailed description of the status of the [BillingClient.queryPurchaseHistory]. final BillingResultWrapper billingResult; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart index 5815a866c82d..ad2a909fbfdc 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart @@ -12,7 +12,9 @@ PurchaseWrapper _$PurchaseWrapperFromJson(Map json) => PurchaseWrapper( purchaseTime: json['purchaseTime'] as int? ?? 0, purchaseToken: json['purchaseToken'] as String? ?? '', signature: json['signature'] as String? ?? '', - sku: json['sku'] as String? ?? '', + skus: + (json['skus'] as List?)?.map((e) => e as String).toList() ?? + [], isAutoRenewing: json['isAutoRenewing'] as bool, originalJson: json['originalJson'] as String? ?? '', developerPayload: json['developerPayload'] as String?, @@ -28,7 +30,9 @@ PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) => purchaseTime: json['purchaseTime'] as int? ?? 0, purchaseToken: json['purchaseToken'] as String? ?? '', signature: json['signature'] as String? ?? '', - sku: json['sku'] as String? ?? '', + skus: + (json['skus'] as List?)?.map((e) => e as String).toList() ?? + [], originalJson: json['originalJson'] as String? ?? '', developerPayload: json['developerPayload'] as String?, ); 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 53595c572901..1c5c2d1fcee9 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 @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -137,7 +135,7 @@ class SkuDetailsWrapper { final int originalPriceAmountMicros; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (other.runtimeType != runtimeType) { return false; } @@ -161,7 +159,7 @@ class SkuDetailsWrapper { @override int get hashCode { - return hashValues( + return Object.hash( description.hashCode, freeTrialPeriod.hashCode, introductoryPrice.hashCode, @@ -205,7 +203,7 @@ class SkuDetailsResponseWrapper { final List skuDetailsList; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (other.runtimeType != runtimeType) { return false; } @@ -216,7 +214,7 @@ class SkuDetailsResponseWrapper { } @override - int get hashCode => hashValues(billingResult, skuDetailsList); + int get hashCode => Object.hash(billingResult, skuDetailsList); } /// Params containing the response code and the debug message from the Play Billing API response. @@ -250,7 +248,7 @@ class BillingResultWrapper { final String? debugMessage; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (other.runtimeType != runtimeType) { return false; } @@ -261,5 +259,5 @@ class BillingResultWrapper { } @override - int get hashCode => hashValues(responseCode, debugMessage); + int get hashCode => Object.hash(responseCode, debugMessage); } 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 61af75688a01..d73ca8ef2e00 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 @@ -6,11 +6,10 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:in_app_purchase_android/in_app_purchase_android.dart'; -import 'package:in_app_purchase_android/src/in_app_purchase_android_platform_addition.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import '../billing_client_wrappers.dart'; +import '../in_app_purchase_android.dart'; /// [IAPError.code] code for failed purchases. const String kPurchaseErrorCode = 'purchase_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 9bcfc3d1b007..d5657d1a38d8 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 @@ -3,11 +3,10 @@ // found in the LICENSE file. import 'package:flutter/services.dart'; -import 'package:in_app_purchase_android/in_app_purchase_android.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import '../billing_client_wrappers.dart'; -import 'types/types.dart'; +import '../in_app_purchase_android.dart'; /// Contains InApp Purchase features that are only available on PlayStore. class InAppPurchaseAndroidPlatformAddition @@ -26,7 +25,9 @@ class InAppPurchaseAndroidPlatformAddition // ignore: deprecated_member_use_from_same_package /// See also [enablePendingPurchases] for more on pending purchases. @Deprecated( - 'The requirement to call `enablePendingPurchases()` has become obsolete since Google Play no longer accepts app submissions that don\'t support pending purchases.') + 'The requirement to call `enablePendingPurchases()` has become obsolete ' + "since Google Play no longer accepts app submissions that don't support " + 'pending purchases.') static bool get enablePendingPurchase => true; /// Enable the [InAppPurchaseConnection] to handle pending purchases. @@ -34,7 +35,9 @@ class InAppPurchaseAndroidPlatformAddition /// **Deprecation warning:** it is no longer required to call /// [enablePendingPurchases] when initializing your application. @Deprecated( - 'The requirement to call `enablePendingPurchases()` has become obsolete since Google Play no longer accepts app submissions that don\'t support pending purchases.') + 'The requirement to call `enablePendingPurchases()` has become obsolete ' + "since Google Play no longer accepts app submissions that don't support " + 'pending purchases.') static void enablePendingPurchases() { // No-op, until it is time to completely remove this method from the API. } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_product_details.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_product_details.dart index 58fd34e0ad55..7affa242055b 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_product_details.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_product_details.dart @@ -2,9 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import '../../billing_client_wrappers.dart'; + /// The class represents the information of a product as registered in at /// Google Play store front. class GooglePlayProductDetails extends ProductDetails { @@ -39,7 +40,7 @@ class GooglePlayProductDetails extends ProductDetails { title: skuDetails.title, description: skuDetails.description, price: skuDetails.price, - rawPrice: ((skuDetails.priceAmountMicros) / 1000000.0).toDouble(), + rawPrice: skuDetails.priceAmountMicros / 1000000.0, currencyCode: skuDetails.priceCurrencyCode, currencySymbol: skuDetails.priceCurrencySymbol, skuDetails: skuDetails, 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 0a667c672945..555773a47910 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.2+2 +version: 0.2.3+6 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: @@ -21,11 +21,12 @@ dependencies: flutter: sdk: flutter in_app_purchase_platform_interface: ^1.3.0 - json_annotation: ^4.3.0 + json_annotation: ^4.6.0 dev_dependencies: build_runner: ^2.0.0 flutter_test: sdk: flutter - json_serializable: ^6.0.0 + json_serializable: ^6.3.1 + mockito: ^5.1.0 test: ^1.16.0 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 1e30ce41beda..4dae957e21eb 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 @@ -95,7 +95,6 @@ void main() { test('handles method channel returning null', () async { stubPlatform.addResponse( name: methodName, - value: null, ); expect( @@ -110,7 +109,7 @@ void main() { test('endConnection', () async { const String endConnectionName = 'BillingClient#endConnection()'; expect(stubPlatform.countPreviousCalls(endConnectionName), equals(0)); - stubPlatform.addResponse(name: endConnectionName, value: null); + stubPlatform.addResponse(name: endConnectionName); await billingClient.endConnection(); expect(stubPlatform.countPreviousCalls(endConnectionName), equals(1)); }); @@ -162,7 +161,7 @@ void main() { }); test('handles null method channel response', () async { - stubPlatform.addResponse(name: queryMethodName, value: null); + stubPlatform.addResponse(name: queryMethodName); final SkuDetailsResponseWrapper response = await billingClient .querySkuDetails( @@ -227,8 +226,7 @@ void main() { sku: skuDetails.sku, accountId: accountId, obfuscatedProfileId: profileId, - oldSku: dummyOldPurchase.sku, - purchaseToken: null), + oldSku: dummyOldPurchase.sku), throwsAssertionError); expect( @@ -236,7 +234,6 @@ void main() { sku: skuDetails.sku, accountId: accountId, obfuscatedProfileId: profileId, - oldSku: null, purchaseToken: dummyOldPurchase.purchaseToken), throwsAssertionError); }); @@ -314,6 +311,45 @@ void main() { const ProrationModeConverter().toJson(prorationMode)); }); + test( + 'serializes and deserializes data when using immediateAndChargeFullPrice', + () async { + const String debugMessage = 'dummy message'; + const BillingResponse responseCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + const SkuDetailsWrapper skuDetails = dummySkuDetails; + const String accountId = 'hashedAccountId'; + const String profileId = 'hashedProfileId'; + const ProrationMode prorationMode = + ProrationMode.immediateAndChargeFullPrice; + + expect( + await billingClient.launchBillingFlow( + sku: skuDetails.sku, + accountId: accountId, + obfuscatedProfileId: profileId, + oldSku: dummyOldPurchase.sku, + prorationMode: prorationMode, + purchaseToken: dummyOldPurchase.purchaseToken), + equals(expectedBillingResult)); + final Map arguments = stubPlatform + .previousCallMatching(launchMethodName) + .arguments as Map; + expect(arguments['sku'], equals(skuDetails.sku)); + expect(arguments['accountId'], equals(accountId)); + expect(arguments['oldSku'], equals(dummyOldPurchase.sku)); + expect(arguments['obfuscatedProfileId'], equals(profileId)); + expect( + arguments['purchaseToken'], equals(dummyOldPurchase.purchaseToken)); + expect(arguments['prorationMode'], + const ProrationModeConverter().toJson(prorationMode)); + }); + test('handles null accountId', () async { const String debugMessage = 'dummy message'; const BillingResponse responseCode = BillingResponse.ok; @@ -337,7 +373,6 @@ void main() { test('handles method channel returning null', () async { stubPlatform.addResponse( name: launchMethodName, - value: null, ); const SkuDetailsWrapper skuDetails = dummySkuDetails; expect( @@ -400,7 +435,6 @@ void main() { test('handles method channel returning null', () async { stubPlatform.addResponse( name: queryPurchasesMethodName, - value: null, ); final PurchasesResultWrapper response = await billingClient.queryPurchases(SkuType.inapp); @@ -466,7 +500,6 @@ void main() { test('handles method channel returning null', () async { stubPlatform.addResponse( name: queryPurchaseHistoryMethodName, - value: null, ); final PurchasesHistoryResult response = await billingClient.queryPurchaseHistory(SkuType.inapp); @@ -501,7 +534,6 @@ void main() { test('handles method channel returning null', () async { stubPlatform.addResponse( name: consumeMethodName, - value: null, ); final BillingResultWrapper billingResult = await billingClient.consumeAsync('dummy token'); @@ -534,7 +566,6 @@ void main() { test('handles method channel returning null', () async { stubPlatform.addResponse( name: acknowledgeMethodName, - value: null, ); final BillingResultWrapper billingResult = await billingClient.acknowledgePurchase('dummy token'); diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart index 65c8bb213cc4..184d9331e6c1 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart @@ -11,7 +11,7 @@ const PurchaseWrapper dummyPurchase = PurchaseWrapper( packageName: 'packageName', purchaseTime: 0, signature: 'signature', - sku: 'sku', + skus: ['sku'], purchaseToken: 'purchaseToken', isAutoRenewing: false, originalJson: '', @@ -27,7 +27,7 @@ const PurchaseWrapper dummyUnacknowledgedPurchase = PurchaseWrapper( packageName: 'packageName', purchaseTime: 0, signature: 'signature', - sku: 'sku', + skus: ['sku'], purchaseToken: 'purchaseToken', isAutoRenewing: false, originalJson: '', @@ -40,7 +40,7 @@ const PurchaseHistoryRecordWrapper dummyPurchaseHistoryRecord = PurchaseHistoryRecordWrapper( purchaseTime: 0, signature: 'signature', - sku: 'sku', + skus: ['sku'], purchaseToken: 'purchaseToken', originalJson: '', developerPayload: 'dummy payload', @@ -51,7 +51,7 @@ const PurchaseWrapper dummyOldPurchase = PurchaseWrapper( packageName: 'oldPackageName', purchaseTime: 0, signature: 'oldSignature', - sku: 'oldSku', + skus: ['oldSku'], purchaseToken: 'oldPurchaseToken', isAutoRenewing: false, originalJson: '', @@ -205,7 +205,7 @@ Map buildPurchaseMap(PurchaseWrapper original) { 'packageName': original.packageName, 'purchaseTime': original.purchaseTime, 'signature': original.signature, - 'sku': original.sku, + 'skus': original.skus, 'purchaseToken': original.purchaseToken, 'isAutoRenewing': original.isAutoRenewing, 'originalJson': original.originalJson, @@ -223,7 +223,7 @@ Map buildPurchaseHistoryRecordMap( return { 'purchaseTime': original.purchaseTime, 'signature': original.signature, - 'sku': original.sku, + 'skus': original.skus, 'purchaseToken': original.purchaseToken, 'originalJson': original.originalJson, 'developerPayload': original.developerPayload, diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart index ecc399b27716..2d1436885427 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart @@ -125,6 +125,60 @@ void main() { expect(billingResult.debugMessage, kInvalidBillingResultErrorMessage); expect(billingResult.responseCode, BillingResponse.error); }); + + test('operator == of SkuDetailsWrapper works fine', () { + const SkuDetailsWrapper firstSkuDetailsInstance = SkuDetailsWrapper( + description: 'description', + freeTrialPeriod: 'freeTrialPeriod', + introductoryPrice: 'introductoryPrice', + introductoryPriceAmountMicros: 990000, + introductoryPriceCycles: 1, + introductoryPricePeriod: 'introductoryPricePeriod', + price: 'price', + priceAmountMicros: 1000, + priceCurrencyCode: 'priceCurrencyCode', + priceCurrencySymbol: r'$', + sku: 'sku', + subscriptionPeriod: 'subscriptionPeriod', + title: 'title', + type: SkuType.inapp, + originalPrice: 'originalPrice', + originalPriceAmountMicros: 1000, + ); + const SkuDetailsWrapper secondSkuDetailsInstance = SkuDetailsWrapper( + description: 'description', + freeTrialPeriod: 'freeTrialPeriod', + introductoryPrice: 'introductoryPrice', + introductoryPriceAmountMicros: 990000, + introductoryPriceCycles: 1, + introductoryPricePeriod: 'introductoryPricePeriod', + price: 'price', + priceAmountMicros: 1000, + priceCurrencyCode: 'priceCurrencyCode', + priceCurrencySymbol: r'$', + sku: 'sku', + subscriptionPeriod: 'subscriptionPeriod', + title: 'title', + type: SkuType.inapp, + originalPrice: 'originalPrice', + originalPriceAmountMicros: 1000, + ); + expect(firstSkuDetailsInstance == secondSkuDetailsInstance, isTrue); + }); + + test('operator == of BillingResultWrapper works fine', () { + const BillingResultWrapper firstBillingResultInstance = + BillingResultWrapper( + responseCode: BillingResponse.ok, + debugMessage: 'debugMessage', + ); + const BillingResultWrapper secondBillingResultInstance = + BillingResultWrapper( + responseCode: BillingResponse.ok, + debugMessage: 'debugMessage', + ); + expect(firstBillingResultInstance == secondBillingResultInstance, isTrue); + }); }); } 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 9d2045b4c229..9737282e27b7 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 @@ -8,7 +8,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; import 'package:in_app_purchase_android/src/channel.dart'; -import 'package:in_app_purchase_android/src/in_app_purchase_android_platform_addition.dart'; import 'billing_client_wrappers/purchase_wrapper_test.dart'; import 'stub_in_app_purchase_platform.dart'; @@ -36,7 +35,7 @@ void main() { stubPlatform.addResponse( name: startConnectionCall, value: buildBillingResultMap(expectedBillingResult)); - stubPlatform.addResponse(name: endConnectionCall, value: null); + stubPlatform.addResponse(name: endConnectionCall); iapAndroidPlatformAddition = InAppPurchaseAndroidPlatformAddition(BillingClient((_) {})); }); 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 b19d631092e7..b6055cc9a8bb 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 @@ -39,7 +39,7 @@ void main() { stubPlatform.addResponse( name: startConnectionCall, value: buildBillingResultMap(expectedBillingResult)); - stubPlatform.addResponse(name: endConnectionCall, value: null); + stubPlatform.addResponse(name: endConnectionCall); InAppPurchaseAndroidPlatform.registerPlatform(); iapAndroidPlatform = @@ -299,7 +299,7 @@ void main() { 'purchasesList': [ { 'orderId': 'orderID1', - 'sku': skuDetails.sku, + 'skus': [skuDetails.sku], 'isAutoRenewing': false, 'packageName': 'package', 'purchaseTime': 1231231231, @@ -401,7 +401,7 @@ void main() { 'purchasesList': [ { 'orderId': 'orderID1', - 'sku': skuDetails.sku, + 'skus': [skuDetails.sku], 'isAutoRenewing': false, 'packageName': 'package', 'purchaseTime': 1231231231, @@ -515,7 +515,7 @@ void main() { 'purchasesList': [ { 'orderId': 'orderID1', - 'sku': skuDetails.sku, + 'skus': [skuDetails.sku], 'isAutoRenewing': false, 'packageName': 'package', 'purchaseTime': 1231231231, @@ -592,7 +592,7 @@ void main() { 'purchasesList': [ { 'orderId': 'orderID1', - 'sku': skuDetails.sku, + 'skus': [skuDetails.sku], 'isAutoRenewing': false, 'packageName': 'package', 'purchaseTime': 1231231231, 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 a52d8d244f5f..17ba02986088 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,9 @@ +## 1.3.2 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. +* Removes unnecessary imports. + ## 1.3.1 * Update to use the `verify` method introduced in plugin_platform_interface 2.1.0. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.dart index 746675549295..e93787e95d43 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import '../in_app_purchase_platform_interface.dart'; // ignore: avoid_classes_with_only_static_members /// The interface that platform implementations must implement when they want to diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition_provider.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition_provider.dart index 642bbb419c6e..adeaa3e53397 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition_provider.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition_provider.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; +import 'in_app_purchase_platform_addition.dart'; /// The [InAppPurchasePlatformAdditionProvider] is responsible for providing /// a platform-specific [InAppPurchasePlatformAddition]. 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 2a0a6bf061d4..46e38b0a03fa 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 @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/in_app_purchas issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%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: 1.3.1 +version: 1.3.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -19,4 +19,3 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.0 - pedantic: ^1.10.0 diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart index 9c0f2dc00020..879ad9c4c633 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart @@ -14,7 +14,14 @@ void main() { test('Cannot be implemented with `implements`', () { expect(() { InAppPurchasePlatform.instance = ImplementsInAppPurchasePlatform(); - }, throwsNoSuchMethodError); + // In versions of `package:plugin_platform_interface` prior to fixing + // https://github.com/flutter/flutter/issues/109339, an attempt to + // implement a platform interface using `implements` would sometimes + // throw a `NoSuchMethodError` and other times throw an + // `AssertionError`. After the issue is fixed, an `AssertionError` will + // always be thrown. For the purpose of this test, we don't really care + // what exception is thrown, so just allow any exception. + }, throwsA(anything)); }); test('Can be extended', () { @@ -126,7 +133,7 @@ void main() { InAppPurchasePlatformAddition.instance = null; }); - test('Cannot be implemented with `implements`', () { + test('Default instance is null', () { expect(InAppPurchasePlatformAddition.instance, isNull); }); diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/types/product_details_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/types/product_details_test.dart index 737f0d00b392..486f38fa850c 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/types/product_details_test.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/types/product_details_test.dart @@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; -import 'package:in_app_purchase_platform_interface/src/types/purchase_status.dart'; void main() { group('Constructor Tests', () { 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 c5df52151411..324e0608b7f9 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,63 @@ +## 0.3.3 + +* Supports adding discount information to AppStorePurchaseParam. +* Fixes iOS Promotional Offers bug which prevents them from working. + +## 0.3.2+2 + +* Updates imports for `prefer_relative_imports`. + +## 0.3.2+1 + +* Updates minimum Flutter version to 2.10. +* Replaces deprecated ThemeData.primaryColor. + +## 0.3.2 + +* Adds the `identifier` and `type` fields to the `SKProductDiscountWrapper` to reflect the changes in the [SKProductDiscount](https://developer.apple.com/documentation/storekit/skproductdiscount?language=objc) in iOS 12.2. + +## 0.3.1+1 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 0.3.1 + +* Adds ability to purchase more than one of a product. + +## 0.3.0+10 + +* Ignores deprecation warnings for upcoming styleFrom button API changes. + +## 0.3.0+9 + +* Updates references to the obsolete master branch. + +## 0.3.0+8 + +* Fixes a memory leak on iOS. + +## 0.3.0+7 + +* Minor fixes for new analysis options. + +## 0.3.0+6 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.3.0+5 + +* Migrates from `ui.hash*` to `Object.hash*`. + +## 0.3.0+4 + +* Ensures that `NSError` instances with an unexpected value for the `userInfo` field don't crash the app, but send an explanatory message instead. + +## 0.3.0+3 + +* Implements transaction caching for StoreKit ensuring transactions are delivered to the Flutter client. + ## 0.3.0+2 * Internal code cleanup for stricter analysis options. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/README.md b/packages/in_app_purchase/in_app_purchase_storekit/README.md index e9585f324331..76e2854c26e1 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/README.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/README.md @@ -21,7 +21,7 @@ editing any of the serialized data structs, rebuild the serializers by running watch the filesystem for changes. If you would like to contribute to the plugin, check out our -[contribution guide](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md). +[contribution guide](https://github.com/flutter/plugins/blob/main/CONTRIBUTING.md). [1]: ../in_app_purchase diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/README.md b/packages/in_app_purchase/in_app_purchase_storekit/example/README.md index 9cf98bf02e79..96b8bb17dbff 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/README.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/README.md @@ -1,75 +1,9 @@ -# In App Purchase iOS Example +# Platform Implementation Test App -Demonstrates how to use the In App Purchase iOS (IAP) Plugin. +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. -## Getting Started - -### Preparation - -There's a significant amount of setup required for testing in app purchases -successfully, including registering new app IDs and store entries to use for -testing in App Store Connect. The App Store requires developers to configure -an app with in-app items for purchase to call their in-app-purchase APIs. -The App Store has extensive documentation on how to do this, and we've also -included a high level guide below. - -* [In-App Purchase (App Store)](https://developer.apple.com/in-app-purchase/) - -### iOS - -When using Xcode 12 and iOS 14 or higher you can run the example in the simulator or on a device without -having to configure an App in App Store Connect. The example app is set up to use StoreKit Testing configured -in the `example/ios/Runner/Configuration.storekit` file (as documented in the article [Setting Up StoreKit Testing in Xcode](https://developer.apple.com/documentation/xcode/setting_up_storekit_testing_in_xcode?language=objc)). -To run the application take the following steps (note that it will only work when running from Xcode): - -1. Open the example app with Xcode, `File > Open File` `example/ios/Runner.xcworkspace`; - -2. Within Xcode edit the current scheme, `Product > Scheme > Edit Scheme...` (or press `Command + Shift + ,`); - -3. Enable StoreKit testing: - a. Select the `Run` action; - b. Click `Options` in the action settings; - c. Select the `Configuration.storekit` for the StoreKit Configuration option. - -4. Click the `Close` button to close the scheme editor; - -5. Select the device you want to run the example App on; - -6. Run the application using `Product > Run` (or hit the run button). - -When testing on pre-iOS 14 you can't run the example app on a simulator and you will need to configure an app in App Store Connect. You can do so by following the steps below: - -1. Follow ["Workflow for configuring in-app - purchases"](https://help.apple.com/app-store-connect/#/devb57be10e7), a - detailed guide on all the steps needed to enable IAPs for an app. Complete - steps 1 ("Sign a Paid Applications Agreement") and 2 ("Configure in-app - purchases"). - - For step #2, "Configure in-app purchases in App Store Connect," you'll want - to create the following products: - - - A consumable with product ID `consumable` - - An upgrade with product ID `upgrade` - - An auto-renewing subscription with product ID `subscription_silver` - - An non-renewing subscription with product ID `subscription_gold` - -2. In XCode, `File > Open File` `example/ios/Runner.xcworkspace`. Update the - Bundle ID to match the Bundle ID of the app created in step #1. - -3. [Create a Sandbox tester - account](https://help.apple.com/app-store-connect/#/dev8b997bee1) to test the - in-app purchases with. - -4. Use `flutter run` to install the app and test it. Note that you need to test - it on a real device instead of a simulator. Next click on one of the products - in the example App, this enables the "SANDBOX ACCOUNT" section in the iOS - settings. You will now be asked to sign in with your sandbox test account to - complete the purchase (no worries you won't be charged). If for some reason - you aren't asked to sign-in or the wrong user is listed, go into the iOS - settings ("Settings" -> "App Store" -> "SANDBOX ACCOUNT") and update your - sandbox account from there. This procedure is explained in great detail in - the [Testing In-App Purchases with Sandbox](https://developer.apple.com/documentation/storekit/in-app_purchase/testing_in-app_purchases_with_sandbox?language=objc) article. - - -**Important:** signing into any production service (including iTunes!) with the -sandbox test account will permanently invalidate it. +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. 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 a88050193053..3977d549af12 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 = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -22,6 +22,7 @@ A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5279297219369C600FF69E6 /* StoreKit.framework */; }; A59001A721E69658004A3E5E /* InAppPurchasePluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A59001A621E69658004A3E5E /* InAppPurchasePluginTests.m */; }; F67646F82681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */; }; + F6995BDD27CF73000050EA78 /* FIATransactionCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F6995BDC27CF73000050EA78 /* FIATransactionCacheTests.m */; }; F78AF3142342BC89008449C7 /* PaymentQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3132342BC89008449C7 /* PaymentQueueTests.m */; }; /* End PBXBuildFile section */ @@ -78,6 +79,7 @@ A59001A821E69658004A3E5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E4F9651425A612301059769C /* 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 = ""; }; F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIAPPaymentQueueDeleteTests.m; sourceTree = ""; }; + F6995BDC27CF73000050EA78 /* FIATransactionCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIATransactionCacheTests.m; sourceTree = ""; }; F6E5D5F926131C4800C68BED /* Configuration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Configuration.storekit; sourceTree = ""; }; F78AF3132342BC89008449C7 /* PaymentQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PaymentQueueTests.m; sourceTree = ""; }; /* End PBXFileReference section */ @@ -190,6 +192,7 @@ F78AF3132342BC89008449C7 /* PaymentQueueTests.m */, 688DE35021F2A5A100EA2684 /* TranslatorTests.m */, F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */, + F6995BDC27CF73000050EA78 /* FIATransactionCacheTests.m */, ); path = RunnerTests; sourceTree = ""; @@ -254,7 +257,7 @@ isa = PBXProject; attributes = { DefaultBuildSystemTypeForWorkspace = Original; - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -406,6 +409,7 @@ F67646F82681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m in Sources */, 6896B34621E9363700D37AEF /* ProductRequestHandlerTests.m in Sources */, 688DE35121F2A5A100EA2684 /* TranslatorTests.m in Sources */, + F6995BDD27CF73000050EA78 /* FIATransactionCacheTests.m in Sources */, A59001A721E69658004A3E5E /* InAppPurchasePluginTests.m in Sources */, 6896B34C21EEB4B800D37AEF /* Stubs.m in Sources */, ); diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3bd47ecb9ec0..a8adf88572cd 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + +@import in_app_purchase_storekit; + +@interface FIATransactionCacheTests : XCTestCase + +@end + +@implementation FIATransactionCacheTests + +- (void)testAddObjectsForNewKey { + NSArray *dummyArray = @[ @1, @2, @3 ]; + FIATransactionCache *cache = [[FIATransactionCache alloc] init]; + [cache addObjects:dummyArray forKey:TransactionCacheKeyUpdatedTransactions]; + + XCTAssertEqual(dummyArray, [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); +} + +- (void)testAddObjectsForExistingKey { + NSArray *dummyArray = @[ @1, @2, @3 ]; + FIATransactionCache *cache = [[FIATransactionCache alloc] init]; + [cache addObjects:dummyArray forKey:TransactionCacheKeyUpdatedTransactions]; + + XCTAssertEqual(dummyArray, [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); + + [cache addObjects:@[ @4, @5, @6 ] forKey:TransactionCacheKeyUpdatedTransactions]; + + NSArray *expected = @[ @1, @2, @3, @4, @5, @6 ]; + XCTAssertEqualObjects(expected, [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); +} + +- (void)testGetObjectsForNonExistingKey { + FIATransactionCache *cache = [[FIATransactionCache alloc] init]; + XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); +} + +- (void)testClear { + NSArray *fakeUpdatedTransactions = @[ @1, @2, @3 ]; + NSArray *fakeRemovedTransactions = @[ @"Remove 1", @"Remove 2", @"Remove 3" ]; + NSArray *fakeUpdatedDownloads = @[ @"Download 1", @"Download 2" ]; + FIATransactionCache *cache = [[FIATransactionCache alloc] init]; + [cache addObjects:fakeUpdatedTransactions forKey:TransactionCacheKeyUpdatedTransactions]; + [cache addObjects:fakeRemovedTransactions forKey:TransactionCacheKeyRemovedTransactions]; + [cache addObjects:fakeUpdatedDownloads forKey:TransactionCacheKeyUpdatedDownloads]; + + XCTAssertEqual(fakeUpdatedTransactions, + [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); + XCTAssertEqual(fakeRemovedTransactions, + [cache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); + XCTAssertEqual(fakeUpdatedDownloads, + [cache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); + + [cache clear]; + + XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); + XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); + XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); +} +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m index fcb2c9425d69..c89589c6a9e5 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m @@ -74,138 +74,84 @@ - (void)testGetProductResponse { XCTAssertTrue([resultArray.firstObject[@"productIdentifier"] isEqualToString:@"123"]); } -- (void)testAddPaymentFailure { +- (void)testAddPaymentShouldReturnFlutterErrorWhenArgumentsAreInvalid { XCTestExpectation *expectation = - [self expectationWithDescription:@"result should return failed state"]; + [self expectationWithDescription: + @"Result should contain a FlutterError when invalid parameters are passed in."]; + NSString *argument = @"Invalid argument"; FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - }]; - SKPaymentQueueStub *queue = [SKPaymentQueueStub new]; - queue.testState = SKPaymentTransactionStateFailed; - __block SKPaymentTransaction *transactionForUpdateBlock; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - if (transaction.transactionState == SKPaymentTransactionStateFailed) { - transactionForUpdateBlock = transaction; - [expectation fulfill]; - } - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:self.plugin.paymentQueueHandler]; - + arguments:argument]; [self.plugin handleMethodCall:call - result:^(id r){ + result:^(id _Nullable result) { + FlutterError *error = result; + XCTAssertEqualObjects(@"storekit_invalid_argument", error.code); + XCTAssertEqualObjects(@"Argument type of addPayment is not a Dictionary", + error.message); + XCTAssertEqualObjects(argument, error.details); + [expectation fulfill]; }]; + [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed); } -- (void)testAddPaymentSuccessWithoutPaymentDiscount { +- (void)testAddPaymentShouldReturnFlutterErrorWhenPaymentFails { + NSDictionary *arguments = @{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + }; XCTestExpectation *expectation = - [self expectationWithDescription:@"result should return success state"]; + [self expectationWithDescription:@"Result should return failed state."]; FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - }]; - SKPaymentQueueStub *queue = [SKPaymentQueueStub new]; - queue.testState = SKPaymentTransactionStatePurchased; - __block SKPaymentTransaction *transactionForUpdateBlock; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - if (transaction.transactionState == SKPaymentTransactionStatePurchased) { - transactionForUpdateBlock = transaction; - if (@available(iOS 12.2, *)) { - XCTAssertNil(transaction.payment.paymentDiscount); - } - [expectation fulfill]; - } - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + arguments:arguments]; + + FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); + OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(NO); + self.plugin.paymentQueueHandler = mockHandler; + [self.plugin handleMethodCall:call - result:^(id r){ + result:^(id _Nullable result) { + FlutterError *error = result; + XCTAssertEqualObjects(@"storekit_duplicate_product_object", error.code); + XCTAssertEqualObjects( + @"There is a pending transaction for the same product identifier. " + @"Please either wait for it to be finished or finish it manually " + @"using `completePurchase` to avoid edge cases.", + error.message); + XCTAssertEqualObjects(arguments, error.details); + [expectation fulfill]; }]; + [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); + OCMVerify(times(1), [mockHandler addPayment:[OCMArg any]]); } -- (void)testAddPaymentSuccessWithPaymentDiscount { +- (void)testAddPaymentSuccessWithoutPaymentDiscount { XCTestExpectation *expectation = - [self expectationWithDescription:@"result should return success state"]; + [self expectationWithDescription:@"Result should return success state"]; FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" arguments:@{ @"productIdentifier" : @"123", @"quantity" : @(1), @"simulatesAskToBuyInSandbox" : @YES, - @"paymentDiscount" : @{ - @"identifier" : @"test_identifier", - @"keyIdentifier" : @"test_key_identifier", - @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003", - @"signature" : @"test_signature", - @"timestamp" : @(1635847102), - } }]; - SKPaymentQueueStub *queue = [SKPaymentQueueStub new]; - queue.testState = SKPaymentTransactionStatePurchased; - __block SKPaymentTransaction *transactionForUpdateBlock; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - if (transaction.transactionState == SKPaymentTransactionStatePurchased) { - transactionForUpdateBlock = transaction; - if (@available(iOS 12.2, *)) { - SKPaymentDiscount *paymentDiscount = transaction.payment.paymentDiscount; - XCTAssertEqual(paymentDiscount.identifier, @"test_identifier"); - XCTAssertEqual(paymentDiscount.keyIdentifier, @"test_key_identifier"); - XCTAssertEqualObjects( - paymentDiscount.nonce, - [[NSUUID alloc] initWithUUIDString:@"4a11a9cc-3bc3-11ec-8d3d-0242ac130003"]); - XCTAssertEqual(paymentDiscount.signature, @"test_signature"); - XCTAssertEqual(paymentDiscount.timestamp, @(1635847102)); - } - [expectation fulfill]; - } - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); + OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES); + self.plugin.paymentQueueHandler = mockHandler; [self.plugin handleMethodCall:call - result:^(id r){ + result:^(id _Nullable result) { + XCTAssertNil(result); + [expectation fulfill]; }]; [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); } -- (void)testAddPaymentFailureWithInvalidPaymentDiscount { +- (void)testAddPaymentSuccessWithPaymentDiscount { XCTestExpectation *expectation = - [self expectationWithDescription:@"result should return success state"]; + [self expectationWithDescription:@"Result should return success state"]; FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" arguments:@{ @@ -213,6 +159,7 @@ - (void)testAddPaymentFailureWithInvalidPaymentDiscount { @"quantity" : @(1), @"simulatesAskToBuyInSandbox" : @YES, @"paymentDiscount" : @{ + @"identifier" : @"test_identifier", @"keyIdentifier" : @"test_key_identifier", @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003", @"signature" : @"test_signature", @@ -220,28 +167,85 @@ - (void)testAddPaymentFailureWithInvalidPaymentDiscount { } }]; - [self.plugin - handleMethodCall:call - result:^(id r) { - XCTAssertTrue([r isKindOfClass:FlutterError.class]); - FlutterError *result = r; - XCTAssertEqualObjects(result.code, @"storekit_invalid_payment_discount_object"); - XCTAssertEqualObjects(result.message, - @"You have requested a payment and specified a payment " - @"discount with invalid properties. When specifying a " - @"payment discount the 'identifier' field is mandatory."); - XCTAssertEqualObjects(result.details, call.arguments); - [expectation fulfill]; - }]; - + FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); + OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES); + self.plugin.paymentQueueHandler = mockHandler; + [self.plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; + OCMVerify( + times(1), + [mockHandler + addPayment:[OCMArg checkWithBlock:^BOOL(id obj) { + SKPayment *payment = obj; + if (@available(iOS 12.2, *)) { + SKPaymentDiscount *discount = payment.paymentDiscount; + + return [discount.identifier isEqual:@"test_identifier"] && + [discount.keyIdentifier isEqual:@"test_key_identifier"] && + [discount.nonce + isEqual:[[NSUUID alloc] + initWithUUIDString:@"4a11a9cc-3bc3-11ec-8d3d-0242ac130003"]] && + [discount.signature isEqual:@"test_signature"] && + [discount.timestamp isEqual:@(1635847102)]; + } + + return YES; + }]]); +} + +- (void)testAddPaymentFailureWithInvalidPaymentDiscount { + // Support for payment discount is only available on iOS 12.2 and higher. + if (@available(iOS 12.2, *)) { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Result should return success state"]; + NSDictionary *arguments = @{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + @"paymentDiscount" : @{ + @"keyIdentifier" : @"test_key_identifier", + @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003", + @"signature" : @"test_signature", + @"timestamp" : @(1635847102), + } + }; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:arguments]; + + FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); + id translator = OCMClassMock(FIAObjectTranslator.class); + + NSString *error = @"Some error occurred"; + OCMStub(ClassMethod([translator + getSKPaymentDiscountFromMap:[OCMArg any] + withError:(NSString __autoreleasing **)[OCMArg setTo:error]])) + .andReturn(nil); + self.plugin.paymentQueueHandler = mockHandler; + [self.plugin + handleMethodCall:call + result:^(id _Nullable result) { + FlutterError *error = result; + XCTAssertEqualObjects(@"storekit_invalid_payment_discount_object", error.code); + XCTAssertEqualObjects( + @"You have requested a payment and specified a " + @"payment discount with invalid properties. Some error occurred", + error.message); + XCTAssertEqualObjects(arguments, error.details); + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + OCMVerify(never(), [mockHandler addPayment:[OCMArg any]]); + } } - (void)testAddPaymentWithNullSandboxArgument { XCTestExpectation *expectation = [self expectationWithDescription:@"result should return success state"]; - XCTestExpectation *simulatesAskToBuyInSandboxExpectation = - [self expectationWithDescription:@"payment isn't simulatesAskToBuyInSandbox"]; FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" arguments:@{ @@ -249,33 +253,19 @@ - (void)testAddPaymentWithNullSandboxArgument { @"quantity" : @(1), @"simulatesAskToBuyInSandbox" : [NSNull null], }]; - SKPaymentQueueStub *queue = [SKPaymentQueueStub new]; - queue.testState = SKPaymentTransactionStatePurchased; - __block SKPaymentTransaction *transactionForUpdateBlock; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - if (transaction.transactionState == SKPaymentTransactionStatePurchased) { - transactionForUpdateBlock = transaction; - [expectation fulfill]; - } - if (!transaction.payment.simulatesAskToBuyInSandbox) { - [simulatesAskToBuyInSandboxExpectation fulfill]; - } - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); + OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES); + self.plugin.paymentQueueHandler = mockHandler; [self.plugin handleMethodCall:call - result:^(id r){ + result:^(id _Nullable result) { + XCTAssertNil(result); + [expectation fulfill]; }]; - [self waitForExpectations:@[ expectation, simulatesAskToBuyInSandboxExpectation ] timeout:5]; - XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); + [self waitForExpectations:@[ expectation ] timeout:5]; + OCMVerify(times(1), [mockHandler addPayment:[OCMArg checkWithBlock:^BOOL(id obj) { + SKPayment *payment = obj; + return !payment.simulatesAskToBuyInSandbox; + }]]); } - (void)testRestoreTransactions { @@ -297,7 +287,8 @@ - (void)testRestoreTransactions { [expectation fulfill]; } shouldAddStorePayment:nil - updatedDownloads:nil]; + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; [queue addTransactionObserver:self.plugin.paymentQueueHandler]; [self.plugin handleMethodCall:call result:^(id r){ @@ -393,13 +384,15 @@ - (void)testGetPendingTransactions { initWithMap:transactionMap] ]); __block NSArray *resultArray; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:mockQueue - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil]; + self.plugin.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:mockQueue + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; [self.plugin handleMethodCall:call result:^(id r) { resultArray = r; @@ -409,46 +402,40 @@ - (void)testGetPendingTransactions { XCTAssertEqualObjects(resultArray, @[ transactionMap ]); } -- (void)testStartAndStopObservingPaymentQueue { +- (void)testStartObservingPaymentQueue { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Should return success result"]; FlutterMethodCall *startCall = [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]" arguments:nil]; - FlutterMethodCall *stopCall = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue stopObservingTransactionQueue]" - arguments:nil]; - - SKPaymentQueueStub *queue = [SKPaymentQueueStub new]; - - self.plugin.paymentQueueHandler = - [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, - SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil]; - - // Check that there is no observer to start with. - XCTAssertNil(queue.observer); - - // Start observing + FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]); + self.plugin.paymentQueueHandler = mockHandler; [self.plugin handleMethodCall:startCall - result:^(id r){ + result:^(id _Nullable result) { + XCTAssertNil(result); + [expectation fulfill]; }]; - // Observer should be set - XCTAssertNotNil(queue.observer); + [self waitForExpectations:@[ expectation ] timeout:5]; + OCMVerify(times(1), [mockHandler startObservingPaymentQueue]); +} - // Stop observing +- (void)testStopObservingPaymentQueue { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Should return success result"]; + FlutterMethodCall *stopCall = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue stopObservingTransactionQueue]" + arguments:nil]; + FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]); + self.plugin.paymentQueueHandler = mockHandler; [self.plugin handleMethodCall:stopCall - result:^(id r){ + result:^(id _Nullable result) { + XCTAssertNil(result); + [expectation fulfill]; }]; - // No observer should be set - XCTAssertNil(queue.observer); + [self waitForExpectations:@[ expectation ] timeout:5]; + OCMVerify(times(1), [mockHandler stopObservingPaymentQueue]); } - (void)testRegisterPaymentQueueDelegate { @@ -464,7 +451,8 @@ - (void)testRegisterPaymentQueueDelegate { restoreTransactionFailed:nil restoreCompletedTransactionsFinished:nil shouldAddStorePayment:nil - updatedDownloads:nil]; + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; // Verify the delegate is nil before we register one. XCTAssertNil(self.plugin.paymentQueueHandler.delegate); @@ -491,7 +479,8 @@ - (void)testRemovePaymentQueueDelegate { restoreTransactionFailed:nil restoreCompletedTransactionsFinished:nil shouldAddStorePayment:nil - updatedDownloads:nil]; + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; self.plugin.paymentQueueHandler.delegate = OCMProtocolMock(@protocol(SKPaymentQueueDelegate)); // Verify the delegate is not nil before removing it. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m index e267be1da54b..2f8d5857c8d8 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m @@ -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 #import #import "Stubs.h" @@ -59,10 +60,11 @@ - (void)testTransactionPurchased { shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { return YES; } - updatedDownloads:nil]; - [queue addTransactionObserver:handler]; + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; SKPayment *payment = [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler startObservingPaymentQueue]; [handler addPayment:payment]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchased); @@ -87,10 +89,12 @@ - (void)testTransactionFailed { shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { return YES; } - updatedDownloads:nil]; - [queue addTransactionObserver:handler]; + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + SKPayment *payment = [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler startObservingPaymentQueue]; [handler addPayment:payment]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateFailed); @@ -115,10 +119,12 @@ - (void)testTransactionRestored { shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { return YES; } - updatedDownloads:nil]; - [queue addTransactionObserver:handler]; + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + SKPayment *payment = [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler startObservingPaymentQueue]; [handler addPayment:payment]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateRestored); @@ -143,10 +149,12 @@ - (void)testTransactionPurchasing { shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { return YES; } - updatedDownloads:nil]; - [queue addTransactionObserver:handler]; + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + SKPayment *payment = [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler startObservingPaymentQueue]; [handler addPayment:payment]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchasing); @@ -171,10 +179,11 @@ - (void)testTransactionDeferred { shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { return YES; } - updatedDownloads:nil]; - [queue addTransactionObserver:handler]; + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; SKPayment *payment = [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler startObservingPaymentQueue]; [handler addPayment:payment]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateDeferred); @@ -201,12 +210,211 @@ - (void)testFinishTransaction { shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { return YES; } - updatedDownloads:nil]; - [queue addTransactionObserver:handler]; + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; SKPayment *payment = [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler startObservingPaymentQueue]; [handler addPayment:payment]; [self waitForExpectations:@[ expectation ] timeout:5]; } +- (void)testStartObservingPaymentQueueShouldNotProcessTransactionsWhenCacheIsEmpty { + FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); + FIAPaymentQueueHandler *handler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[[SKPaymentQueueStub alloc] init] + transactionsUpdated:^(NSArray *_Nonnull transactions) { + XCTFail("transactionsUpdated callback should not be called when cache is empty."); + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + XCTFail("transactionRemoved callback should not be called when cache is empty."); + } + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:^(NSArray *_Nonnull downloads) { + XCTFail("updatedDownloads callback should not be called when cache is empty."); + } + transactionCache:mockCache]; + + [handler startObservingPaymentQueue]; + + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); +} + +- (void) + testStartObservingPaymentQueueShouldNotProcessTransactionsWhenCacheContainsEmptyTransactionArrays { + FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); + FIAPaymentQueueHandler *handler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[[SKPaymentQueueStub alloc] init] + transactionsUpdated:^(NSArray *_Nonnull transactions) { + XCTFail("transactionsUpdated callback should not be called when cache is empty."); + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + XCTFail("transactionRemoved callback should not be called when cache is empty."); + } + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:^(NSArray *_Nonnull downloads) { + XCTFail("updatedDownloads callback should not be called when cache is empty."); + } + transactionCache:mockCache]; + + OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]).andReturn(@[]); + OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]).andReturn(@[]); + OCMStub([mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]).andReturn(@[]); + + [handler startObservingPaymentQueue]; + + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); +} + +- (void)testStartObservingPaymentQueueShouldProcessTransactionsForItemsInCache { + XCTestExpectation *updateTransactionsExpectation = + [self expectationWithDescription: + @"transactionsUpdated callback should be called with one transaction."]; + XCTestExpectation *removeTransactionsExpectation = + [self expectationWithDescription: + @"transactionsRemoved callback should be called with one transaction."]; + XCTestExpectation *updateDownloadsExpectation = + [self expectationWithDescription: + @"downloadsUpdated callback should be called with one transaction."]; + SKPaymentTransaction *mockTransaction = OCMClassMock(SKPaymentTransaction.class); + SKDownload *mockDownload = OCMClassMock(SKDownload.class); + FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); + FIAPaymentQueueHandler *handler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[[SKPaymentQueueStub alloc] init] + transactionsUpdated:^(NSArray *_Nonnull transactions) { + XCTAssertEqualObjects(transactions, @[ mockTransaction ]); + [updateTransactionsExpectation fulfill]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + XCTAssertEqualObjects(transactions, @[ mockTransaction ]); + [removeTransactionsExpectation fulfill]; + } + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:^(NSArray *_Nonnull downloads) { + XCTAssertEqualObjects(downloads, @[ mockDownload ]); + [updateDownloadsExpectation fulfill]; + } + transactionCache:mockCache]; + + OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]).andReturn(@[ + mockTransaction + ]); + OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]).andReturn(@[ + mockDownload + ]); + OCMStub([mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]).andReturn(@[ + mockTransaction + ]); + + [handler startObservingPaymentQueue]; + + [self waitForExpectations:@[ + updateTransactionsExpectation, removeTransactionsExpectation, updateDownloadsExpectation + ] + timeout:5]; + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); + OCMVerify(times(1), [mockCache clear]); +} + +- (void)testTransactionsShouldBeCachedWhenNotObserving { + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + XCTFail("transactionsUpdated callback should not be called when cache is empty."); + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + XCTFail("transactionRemoved callback should not be called when cache is empty."); + } + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:^(NSArray *_Nonnull downloads) { + XCTFail("updatedDownloads callback should not be called when cache is empty."); + } + transactionCache:mockCache]; + + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler addPayment:payment]; + + OCMVerify(times(1), [mockCache addObjects:[OCMArg any] + forKey:TransactionCacheKeyUpdatedTransactions]); + OCMVerify(never(), [mockCache addObjects:[OCMArg any] + forKey:TransactionCacheKeyUpdatedDownloads]); + OCMVerify(never(), [mockCache addObjects:[OCMArg any] + forKey:TransactionCacheKeyRemovedTransactions]); +} + +- (void)testTransactionsShouldNotBeCachedWhenObserving { + XCTestExpectation *updateTransactionsExpectation = + [self expectationWithDescription: + @"transactionsUpdated callback should be called with one transaction."]; + XCTestExpectation *removeTransactionsExpectation = + [self expectationWithDescription: + @"transactionsRemoved callback should be called with one transaction."]; + XCTestExpectation *updateDownloadsExpectation = + [self expectationWithDescription: + @"downloadsUpdated callback should be called with one transaction."]; + SKPaymentTransaction *mockTransaction = OCMClassMock(SKPaymentTransaction.class); + SKDownload *mockDownload = OCMClassMock(SKDownload.class); + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStatePurchased; + FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + XCTAssertEqualObjects(transactions, @[ mockTransaction ]); + [updateTransactionsExpectation fulfill]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + XCTAssertEqualObjects(transactions, @[ mockTransaction ]); + [removeTransactionsExpectation fulfill]; + } + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:^(NSArray *_Nonnull downloads) { + XCTAssertEqualObjects(downloads, @[ mockDownload ]); + [updateDownloadsExpectation fulfill]; + } + transactionCache:mockCache]; + + [handler startObservingPaymentQueue]; + [handler paymentQueue:queue updatedTransactions:@[ mockTransaction ]]; + [handler paymentQueue:queue removedTransactions:@[ mockTransaction ]]; + [handler paymentQueue:queue updatedDownloads:@[ mockDownload ]]; + + [self waitForExpectations:@[ + updateTransactionsExpectation, removeTransactionsExpectation, updateDownloadsExpectation + ] + timeout:5]; + OCMVerify(never(), [mockCache addObjects:[OCMArg any] + forKey:TransactionCacheKeyUpdatedTransactions]); + OCMVerify(never(), [mockCache addObjects:[OCMArg any] + forKey:TransactionCacheKeyUpdatedDownloads]); + OCMVerify(never(), [mockCache addObjects:[OCMArg any] + forKey:TransactionCacheKeyRemovedTransactions]); +} @end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m index e4277d3edd59..f5e44d78b157 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m @@ -31,6 +31,10 @@ - (instancetype)initWithMap:(NSDictionary *)map { [[SKProductSubscriptionPeriodStub alloc] initWithMap:map[@"subscriptionPeriod"]]; [self setValue:subscriptionPeriodSub forKey:@"subscriptionPeriod"]; [self setValue:map[@"paymentMode"] ?: @(0) forKey:@"paymentMode"]; + if (@available(iOS 12.2, *)) { + [self setValue:map[@"identifier"] ?: [NSNull null] forKey:@"identifier"]; + [self setValue:map[@"type"] ?: @(0) forKey:@"type"]; + } } return self; } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m index 0f689f602de8..34d686753762 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m @@ -10,7 +10,8 @@ @interface TranslatorTest : XCTestCase @property(strong, nonatomic) NSDictionary *periodMap; -@property(strong, nonatomic) NSDictionary *discountMap; +@property(strong, nonatomic) NSMutableDictionary *discountMap; +@property(strong, nonatomic) NSMutableDictionary *discountMissingIdentifierMap; @property(strong, nonatomic) NSMutableDictionary *productMap; @property(strong, nonatomic) NSDictionary *productResponseMap; @property(strong, nonatomic) NSDictionary *paymentMap; @@ -27,13 +28,27 @@ @implementation TranslatorTest - (void)setUp { self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; - self.discountMap = @{ + + self.discountMap = [[NSMutableDictionary alloc] initWithDictionary:@{ @"price" : @"1", @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], @"numberOfPeriods" : @1, @"subscriptionPeriod" : self.periodMap, - @"paymentMode" : @1 - }; + @"paymentMode" : @1, + }]; + if (@available(iOS 12.2, *)) { + self.discountMap[@"identifier"] = @"test offer id"; + self.discountMap[@"type"] = @(SKProductDiscountTypeIntroductory); + } + self.discountMissingIdentifierMap = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"price" : @"1", + @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], + @"numberOfPeriods" : @1, + @"subscriptionPeriod" : self.periodMap, + @"paymentMode" : @1, + @"identifier" : [NSNull null], + @"type" : @0, + }]; self.productMap = [[NSMutableDictionary alloc] initWithDictionary:@{ @"price" : @"1", @@ -158,6 +173,56 @@ - (void)testError { XCTAssertEqualObjects(map, self.errorMap); } +- (void)testErrorWithNSNumberAsUserInfo { + NSError *error = [NSError errorWithDomain:SKErrorDomain code:3 userInfo:@{@"key" : @42}]; + NSDictionary *expectedMap = + @{@"domain" : SKErrorDomain, @"code" : @3, @"userInfo" : @{@"key" : @42}}; + NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; + XCTAssertEqualObjects(expectedMap, map); +} + +- (void)testErrorWithMultipleUnderlyingErrors { + NSError *underlyingErrorOne = [NSError errorWithDomain:SKErrorDomain code:2 userInfo:nil]; + NSError *underlyingErrorTwo = [NSError errorWithDomain:SKErrorDomain code:1 userInfo:nil]; + NSError *mainError = [NSError + errorWithDomain:SKErrorDomain + code:3 + userInfo:@{@"underlyingErrors" : @[ underlyingErrorOne, underlyingErrorTwo ]}]; + NSDictionary *expectedMap = @{ + @"domain" : SKErrorDomain, + @"code" : @3, + @"userInfo" : @{ + @"underlyingErrors" : @[ + @{@"domain" : SKErrorDomain, @"code" : @2, @"userInfo" : @{}}, + @{@"domain" : SKErrorDomain, @"code" : @1, @"userInfo" : @{}} + ] + } + }; + NSDictionary *map = [FIAObjectTranslator getMapFromNSError:mainError]; + XCTAssertEqualObjects(expectedMap, map); +} + +- (void)testErrorWithUnsupportedUserInfo { + NSError *error = [NSError errorWithDomain:SKErrorDomain + code:3 + userInfo:@{@"user_info" : [[NSObject alloc] init]}]; + NSDictionary *expectedMap = @{ + @"domain" : SKErrorDomain, + @"code" : @3, + @"userInfo" : @{ + @"user_info" : [NSString + stringWithFormat: + @"Unable to encode native userInfo object of type %@ to map. Please submit an " + @"issue at https://github.com/flutter/flutter/issues/new with the title " + @"\"[in_app_purchase_storekit] Unable to encode userInfo of type %@\" and add " + @"reproduction steps and the error details in the description field.", + [NSObject class], [NSObject class]] + } + }; + NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; + XCTAssertEqualObjects(expectedMap, map); +} + - (void)testLocaleToMap { if (@available(iOS 10.0, *)) { NSLocale *system = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; @@ -224,6 +289,15 @@ - (void)testSKPaymentDiscountFromMapMissingIdentifier { } } +- (void)testGetMapFromSKProductDiscountMissingIdentifier { + if (@available(iOS 12.2, *)) { + SKProductDiscountStub *discount = + [[SKProductDiscountStub alloc] initWithMap:self.discountMissingIdentifierMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductDiscount:discount]; + XCTAssertEqualObjects(map, self.discountMissingIdentifierMap); + } +} + - (void)testSKPaymentDiscountFromMapMissingKeyIdentifier { if (@available(iOS 12.2, *)) { NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; @@ -316,4 +390,27 @@ - (void)testSKPaymentDiscountFromMapMissingTimestamp { } } +- (void)testSKPaymentDiscountFromMapOverflowingTimestamp { + if (@available(iOS 12.2, *)) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : @1665044583595, // timestamp 2022 Oct + }; + NSString *error = nil; + SKPaymentDiscount *paymentDiscount = + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + XCTAssertNil(error); + XCTAssertNotNil(paymentDiscount); + XCTAssertEqual(paymentDiscount.identifier, discountMap[@"identifier"]); + XCTAssertEqual(paymentDiscount.keyIdentifier, discountMap[@"keyIdentifier"]); + XCTAssertEqualObjects(paymentDiscount.nonce, + [[NSUUID alloc] initWithUUIDString:discountMap[@"nonce"]]); + XCTAssertEqual(paymentDiscount.signature, discountMap[@"signature"]); + XCTAssertEqual(paymentDiscount.timestamp, discountMap[@"timestamp"]); + } +} + @end 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 2ee2deb7fe35..09058ea2e89a 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 @@ -3,14 +3,13 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; -import 'package:in_app_purchase_storekit_example/example_payment_queue_delegate.dart'; import 'consumable_store.dart'; +import 'example_payment_queue_delegate.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -22,8 +21,6 @@ void main() { runApp(_MyApp()); } -const bool _kAutoConsume = true; - const String _kConsumableId = 'consumable'; const String _kUpgradeId = 'upgrade'; const String _kSilverSubscriptionId = 'subscription_silver'; @@ -37,7 +34,7 @@ const List _kProductIds = [ class _MyApp extends StatefulWidget { @override - _MyAppState createState() => _MyAppState(); + State<_MyApp> createState() => _MyAppState(); } class _MyAppState extends State<_MyApp> { @@ -191,9 +188,11 @@ class _MyAppState extends State<_MyApp> { } final Widget storeHeader = ListTile( leading: Icon(_isAvailable ? Icons.check : Icons.block, - color: _isAvailable ? Colors.green : ThemeData.light().errorColor), - title: Text( - 'The store is ' + (_isAvailable ? 'available' : 'unavailable') + '.'), + color: _isAvailable + ? Colors.green + : ThemeData.light().colorScheme.error), + title: + Text('The store is ${_isAvailable ? 'available' : 'unavailable'}.'), ); final List children = [storeHeader]; @@ -202,7 +201,7 @@ class _MyAppState extends State<_MyApp> { const Divider(), ListTile( title: Text('Not connected', - style: TextStyle(color: ThemeData.light().errorColor)), + style: TextStyle(color: ThemeData.light().colorScheme.error)), subtitle: const Text( 'Unable to connect to the payments processor. Has this app been configured correctly? See the example README for instructions.'), ), @@ -226,7 +225,7 @@ class _MyAppState extends State<_MyApp> { if (_notFoundIds.isNotEmpty) { productList.add(ListTile( title: Text('[${_notFoundIds.join(", ")}] not found', - style: TextStyle(color: ThemeData.light().errorColor)), + style: TextStyle(color: ThemeData.light().colorScheme.error)), subtitle: const Text( 'This app needs special configuration to run. Please see example/README.md for instructions.'))); } @@ -259,25 +258,25 @@ class _MyAppState extends State<_MyApp> { }, icon: const Icon(Icons.upgrade)) : TextButton( - child: Text(productDetails.price), style: TextButton.styleFrom( backgroundColor: Colors.green[800], + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use primary: Colors.white, ), onPressed: () { final PurchaseParam purchaseParam = PurchaseParam( productDetails: productDetails, - applicationUserName: null, ); if (productDetails.id == _kConsumableId) { _iapStoreKitPlatform.buyConsumable( - purchaseParam: purchaseParam, - autoConsume: _kAutoConsume || Platform.isIOS); + purchaseParam: purchaseParam); } else { _iapStoreKitPlatform.buyNonConsumable( purchaseParam: purchaseParam); } }, + child: Text(productDetails.price), )); }, )); @@ -318,9 +317,9 @@ class _MyAppState extends State<_MyApp> { const Divider(), GridView.count( crossAxisCount: 5, - children: tokens, shrinkWrap: true, padding: const EdgeInsets.all(16.0), + children: tokens, ) ])); } @@ -333,16 +332,17 @@ class _MyAppState extends State<_MyApp> { return Padding( padding: const EdgeInsets.all(4.0), child: Row( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - child: const Text('Restore purchases'), style: TextButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, + backgroundColor: Theme.of(context).colorScheme.primary, + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use primary: Colors.white, ), onPressed: () => _iapStoreKitPlatform.restorePurchases(), + child: const Text('Restore purchases'), ), ], ), 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 597dfb0703bb..e71b85d4b447 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.5.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -22,6 +22,8 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/test_driver/integration_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/test_driver/integration_test.dart @@ -0,0 +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. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); 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 3ceb512abb10..c656b58808b3 100644 --- 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 @@ -74,8 +74,12 @@ + (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount { @"subscriptionPeriod" : [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:discount.subscriptionPeriod] ?: [NSNull null], - @"paymentMode" : @(discount.paymentMode) + @"paymentMode" : @(discount.paymentMode), }]; + if (@available(iOS 12.2, *)) { + [map setObject:discount.identifier ?: [NSNull null] forKey:@"identifier"]; + [map setObject:@(discount.type) forKey:@"type"]; + } // TODO(cyanglaz): NSLocale is a complex object, want to see the actual need of getting this // expanded to a map. Matching android to only get the currencySymbol for now. @@ -167,20 +171,43 @@ + (NSDictionary *)getMapFromNSError:(NSError *)error { if (!error) { return nil; } + NSMutableDictionary *userInfo = [NSMutableDictionary new]; for (NSErrorUserInfoKey key in error.userInfo) { id value = error.userInfo[key]; - if ([value isKindOfClass:[NSError class]]) { - userInfo[key] = [FIAObjectTranslator getMapFromNSError:value]; - } else if ([value isKindOfClass:[NSURL class]]) { - userInfo[key] = [value absoluteString]; - } else { - userInfo[key] = value; - } + userInfo[key] = [FIAObjectTranslator encodeNSErrorUserInfo:value]; } return @{@"code" : @(error.code), @"domain" : error.domain ?: @"", @"userInfo" : userInfo}; } ++ (id)encodeNSErrorUserInfo:(id)value { + if ([value isKindOfClass:[NSError class]]) { + return [FIAObjectTranslator getMapFromNSError:value]; + } else if ([value isKindOfClass:[NSURL class]]) { + return [value absoluteString]; + } else if ([value isKindOfClass:[NSNumber class]]) { + return value; + } else if ([value isKindOfClass:[NSString class]]) { + return value; + } else if ([value isKindOfClass:[NSArray class]]) { + NSMutableArray *errors = [[NSMutableArray alloc] init]; + for (id error in value) { + [errors addObject:[FIAObjectTranslator encodeNSErrorUserInfo:error]]; + } + return errors; + } else { + return [NSString + stringWithFormat: + @"Unable to encode native userInfo object of type %@ to map. Please submit an issue at " + @"https://github.com/flutter/flutter/issues/new with the title " + @"\"[in_app_purchase_storekit] " + @"Unable to encode userInfo of type %@\" and add reproduction steps and the error " + @"details in " + @"the description field.", + [value class], [value class]]; + } +} + + (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront { if (!storefront) { return nil; @@ -250,7 +277,7 @@ + (SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map return nil; } - if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp intValue] <= 0) { + if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp longLongValue] <= 0) { if (error) { *error = @"When specifying a payment discount the 'timestamp' field is mandatory."; } 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 8019831d6355..bb074aa6c577 100644 --- 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 @@ -4,6 +4,7 @@ #import #import +#import "FIATransactionCache.h" @class SKPaymentTransaction; @@ -21,6 +22,27 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); @property(NS_NONATOMIC_IOSONLY, weak, nullable) id delegate API_AVAILABLE( ios(13.0), macos(10.15), watchos(6.2)); +/// Creates a new FIAPaymentQueueHandler initialized with an empty +/// FIATransactionCache. +/// +/// @param queue The SKPaymentQueue instance connected to the App Store and +/// responsible for processing transactions. +/// @param transactionsUpdated Callback method that is called each time the App +/// Store indicates transactions are updated. +/// @param transactionsRemoved Callback method that is called each time the App +/// Store indicates transactions are removed. +/// @param restoreTransactionFailed Callback method that is called each time +/// the App Store indicates transactions failed +/// to restore. +/// @param restoreCompletedTransactionsFinished Callback method that is called +/// each time the App Store +/// indicates restoring of +/// transactions has finished. +/// @param shouldAddStorePayment Callback method that is called each time an +/// in-app purchase has been initiated from the +/// App Store. +/// @param updatedDownloads Callback method that is called each time the App +/// Store indicates downloads are updated. - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved @@ -28,7 +50,57 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); restoreCompletedTransactionsFinished: (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment - updatedDownloads:(nullable UpdatedDownloads)updatedDownloads; + updatedDownloads:(nullable UpdatedDownloads)updatedDownloads + DEPRECATED_MSG_ATTRIBUTE( + "Use the " + "'initWithQueue:transactionsUpdated:transactionsRemoved:restoreTransactionsFinished:" + "shouldAddStorePayment:updatedDownloads:transactionCache:' message instead."); + +/// Creates a new FIAPaymentQueueHandler. +/// +/// The "transactionsUpdated", "transactionsRemoved" and "updatedDownloads" +/// callbacks are only called while actively observing transactions. To start +/// observing transactions send the "startObservingPaymentQueue" message. +/// Sending the "stopObservingPaymentQueue" message will stop actively +/// observing transactions. When transactions are not observed they are cached +/// to the "transactionCache" and will be delivered via the +/// "transactionsUpdated", "transactionsRemoved" and "updatedDownloads" +/// callbacks as soon as the "startObservingPaymentQueue" message arrives. +/// +/// Note: cached transactions that are not processed when the application is +/// killed will be delivered again by the App Store as soon as the application +/// starts again. +/// +/// @param queue The SKPaymentQueue instance connected to the App Store and +/// responsible for processing transactions. +/// @param transactionsUpdated Callback method that is called each time the App +/// Store indicates transactions are updated. +/// @param transactionsRemoved Callback method that is called each time the App +/// Store indicates transactions are removed. +/// @param restoreTransactionFailed Callback method that is called each time +/// the App Store indicates transactions failed +/// to restore. +/// @param restoreCompletedTransactionsFinished Callback method that is called +/// each time the App Store +/// indicates restoring of +/// transactions has finished. +/// @param shouldAddStorePayment Callback method that is called each time an +/// in-app purchase has been initiated from the +/// App Store. +/// @param updatedDownloads Callback method that is called each time the App +/// Store indicates downloads are updated. +/// @param transactionCache An empty [FIATransactionCache] instance that is +/// responsible for keeping track of transactions that +/// arrive when not actively observing transactions. +- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue + transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated + transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved + restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed + restoreCompletedTransactionsFinished: + (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished + shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment + updatedDownloads:(nullable UpdatedDownloads)updatedDownloads + transactionCache:(nonnull FIATransactionCache *)transactionCache; // Can throw exceptions if the transaction type is purchasing, should always used in a @try block. - (void)finishTransaction:(nonnull SKPaymentTransaction *)transaction; - (void)restoreTransactions:(nullable NSString *)applicationName; 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 21667954cf8d..59fdceded2bc 100644 --- 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 @@ -4,18 +4,49 @@ #import "FIAPaymentQueueHandler.h" #import "FIAPPaymentQueueDelegate.h" +#import "FIATransactionCache.h" @interface FIAPaymentQueueHandler () +/// The SKPaymentQueue instance connected to the App Store and responsible for processing +/// transactions. @property(strong, nonatomic) SKPaymentQueue *queue; + +/// Callback method that is called each time the App Store indicates transactions are updated. @property(nullable, copy, nonatomic) TransactionsUpdated transactionsUpdated; + +/// Callback method that is called each time the App Store indicates transactions are removed. @property(nullable, copy, nonatomic) TransactionsRemoved transactionsRemoved; + +/// Callback method that is called each time the App Store indicates transactions failed to restore. @property(nullable, copy, nonatomic) RestoreTransactionFailed restoreTransactionFailed; + +/// Callback method that is called each time the App Store indicates restoring of transactions has +/// finished. @property(nullable, copy, nonatomic) RestoreCompletedTransactionsFinished paymentQueueRestoreCompletedTransactionsFinished; + +/// Callback method that is called each time an in-app purchase has been initiated from the App +/// Store. @property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment; + +/// Callback method that is called each time the App Store indicates downloads are updated. @property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads; +/// The transaction cache responsible for caching transactions. +/// +/// Keeps track of transactions that arrive when the Flutter client is not +/// actively observing for transactions. +@property(strong, nonatomic, nonnull) FIATransactionCache *transactionCache; + +/// Indicates if the Flutter client is observing transactions. +/// +/// When the client is not observing, transactions are cached and send to the +/// client as soon as it starts observing. The Flutter client can start +/// observing by sending a startObservingPaymentQueue message and stop by +/// sending a stopObservingPaymentQueue message. +@property(atomic, assign, readwrite, getter=isObservingTransactions) BOOL observingTransactions; + @end @implementation FIAPaymentQueueHandler @@ -28,6 +59,25 @@ - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment updatedDownloads:(nullable UpdatedDownloads)updatedDownloads { + return [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:transactionsUpdated + transactionRemoved:transactionsRemoved + restoreTransactionFailed:restoreTransactionFailed + restoreCompletedTransactionsFinished:restoreCompletedTransactionsFinished + shouldAddStorePayment:shouldAddStorePayment + updatedDownloads:updatedDownloads + transactionCache:[[FIATransactionCache alloc] init]]; +} + +- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue + transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated + transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved + restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed + restoreCompletedTransactionsFinished: + (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished + shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment + updatedDownloads:(nullable UpdatedDownloads)updatedDownloads + transactionCache:(nonnull FIATransactionCache *)transactionCache { self = [super init]; if (self) { _queue = queue; @@ -37,7 +87,9 @@ - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue _paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished; _shouldAddStorePayment = shouldAddStorePayment; _updatedDownloads = updatedDownloads; + _transactionCache = transactionCache; + [_queue addTransactionObserver:self]; if (@available(iOS 13.0, macOS 10.15, *)) { queue.delegate = self.delegate; } @@ -46,11 +98,43 @@ - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue } - (void)startObservingPaymentQueue { - [_queue addTransactionObserver:self]; + self.observingTransactions = YES; + + [self processCachedTransactions]; } - (void)stopObservingPaymentQueue { - [_queue removeTransactionObserver:self]; + // When the client stops observing transaction, the transaction observer is + // not removed from the SKPaymentQueue. The FIAPaymentQueueHandler will cache + // trasnactions in memory when the client is not observing, allowing the app + // to process these transactions if it starts observing again during the same + // lifetime of the app. + // + // If the app is killed, cached transactions will be removed from memory; + // however, the App Store will re-deliver the transactions as soon as the app + // is started again, since the cached transactions have not been acknowledged + // by the client (by sending the `finishTransaction` message). + self.observingTransactions = NO; +} + +- (void)processCachedTransactions { + NSArray *cachedObjects = + [self.transactionCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]; + if (cachedObjects.count != 0) { + self.transactionsUpdated(cachedObjects); + } + + cachedObjects = [self.transactionCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]; + if (cachedObjects.count != 0) { + self.updatedDownloads(cachedObjects); + } + + cachedObjects = [self.transactionCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]; + if (cachedObjects.count != 0) { + self.transactionsRemoved(cachedObjects); + } + + [self.transactionCache clear]; } - (BOOL)addPayment:(SKPayment *)payment { @@ -93,6 +177,11 @@ - (void)showPriceConsentIfNeeded { // state of transactions and finish as appropriate. - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { + if (!self.observingTransactions) { + [_transactionCache addObjects:transactions forKey:TransactionCacheKeyUpdatedTransactions]; + return; + } + // notify dart through callbacks. self.transactionsUpdated(transactions); } @@ -100,6 +189,10 @@ - (void)paymentQueue:(SKPaymentQueue *)queue // Sent when transactions are removed from the queue (via finishTransaction:). - (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions { + if (!self.observingTransactions) { + [_transactionCache addObjects:transactions forKey:TransactionCacheKeyRemovedTransactions]; + return; + } self.transactionsRemoved(transactions); } @@ -118,6 +211,10 @@ - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue // Sent when the download state has changed. - (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads { + if (!self.observingTransactions) { + [_transactionCache addObjects:downloads forKey:TransactionCacheKeyUpdatedDownloads]; + return; + } self.updatedDownloads(downloads); } 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 new file mode 100644 index 000000000000..dea3c2d85d14 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h @@ -0,0 +1,31 @@ +// 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. + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, TransactionCacheKey) { + TransactionCacheKeyUpdatedDownloads, + TransactionCacheKeyUpdatedTransactions, + TransactionCacheKeyRemovedTransactions +}; + +@interface FIATransactionCache : NSObject + +/// Adds objects to the transaction cache. +/// +/// If the cache already contains an array of objects on the specified key, the supplied +/// array will be appended to the existing array. +- (void)addObjects:(NSArray *)objects forKey:(TransactionCacheKey)key; + +/// Gets the array of objects stored at the given key. +/// +/// If there are no objects associated with the given key nil is returned. +- (NSArray *)getObjectsForKey:(TransactionCacheKey)key; + +/// Removes all objects from the transaction cache. +- (void)clear; + +@end + +NS_ASSUME_NONNULL_END 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 new file mode 100644 index 000000000000..f80b9c40c7bc --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m @@ -0,0 +1,40 @@ +// 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 "FIATransactionCache.h" + +@interface FIATransactionCache () + +/// A NSMutableDictionary storing the objects that are cached. +@property(nonatomic, strong, nonnull) NSMutableDictionary *cache; + +@end + +@implementation FIATransactionCache + +- (instancetype)init { + self = [super init]; + if (self) { + self.cache = [[NSMutableDictionary alloc] init]; + } + + return self; +} + +- (void)addObjects:(NSArray *)objects forKey:(TransactionCacheKey)key { + NSArray *cachedObjects = self.cache[@(key)]; + + self.cache[@(key)] = + cachedObjects ? [cachedObjects arrayByAddingObjectsFromArray:objects] : objects; +} + +- (NSArray *)getObjectsForKey:(TransactionCacheKey)key { + return self.cache[@(key)]; +} + +- (void)clear { + [self.cache removeAllObjects]; +} + +@end 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 661f57f432d8..bfc90ea43716 100644 --- 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 @@ -25,9 +25,6 @@ @interface InAppPurchasePlugin () // Callback channel to dart used for when a function from the payment queue delegate is triggered. @property(strong, nonatomic, readonly) FlutterMethodChannel *paymentQueueDelegateCallbackChannel; - -@property(strong, nonatomic, readonly) NSObject *registry; -@property(strong, nonatomic, readonly) NSObject *messenger; @property(strong, nonatomic, readonly) NSObject *registrar; @property(strong, nonatomic, readonly) FIAPReceiptManager *receiptManager; @@ -57,8 +54,6 @@ - (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager { - (instancetype)initWithRegistrar:(NSObject *)registrar { self = [self initWithReceiptManager:[FIAPReceiptManager new]]; _registrar = registrar; - _registry = [registrar textures]; - _messenger = [registrar messenger]; __weak typeof(self) weakSelf = self; _paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueue defaultQueue] @@ -79,7 +74,8 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar } updatedDownloads:^void(NSArray *_Nonnull downloads) { [weakSelf updatedDownloads:downloads]; - }]; + } + transactionCache:[[FIATransactionCache alloc] init]]; _transactionObserverCallbackChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" @@ -204,10 +200,11 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { : [simulatesAskToBuyInSandbox boolValue]; if (@available(iOS 12.2, *)) { + NSDictionary *paymentDiscountMap = [self getNonNullValueFromDictionary:paymentMap + forKey:@"paymentDiscount"]; NSString *error = nil; - SKPaymentDiscount *paymentDiscount = [FIAObjectTranslator - getSKPaymentDiscountFromMap:[paymentMap objectForKey:@"paymentDiscount"] - withError:&error]; + SKPaymentDiscount *paymentDiscount = + [FIAObjectTranslator getSKPaymentDiscountFromMap:paymentDiscountMap withError:&error]; if (error) { result([FlutterError @@ -346,7 +343,7 @@ - (void)registerPaymentQueueDelegate:(FlutterResult)result { if (@available(iOS 13.0, *)) { _paymentQueueDelegateCallbackChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_payment_queue_delegate" - binaryMessenger:_messenger]; + binaryMessenger:[_registrar messenger]]; _paymentQueueDelegate = [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:_paymentQueueDelegateCallbackChannel]; @@ -371,6 +368,11 @@ - (void)showPriceConsentIfNeeded:(FlutterResult)result { result(nil); } +- (id)getNonNullValueFromDictionary:(NSDictionary *)dictionary forKey:(NSString *)key { + id value = dictionary[key]; + return [value isKindOfClass:[NSNull class]] ? nil : value; +} + #pragma mark - transaction observer: - (void)handleTransactionsUpdated:(NSArray *)transactions { diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart index 6db2e59e1485..0e5e420ece85 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart @@ -7,7 +7,6 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; -import 'package:in_app_purchase_storekit/src/in_app_purchase_storekit_platform_addition.dart'; import '../in_app_purchase_storekit.dart'; import '../store_kit_wrappers.dart'; @@ -72,11 +71,14 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { Future buyNonConsumable({required PurchaseParam purchaseParam}) async { await _skPaymentQueueWrapper.addPayment(SKPaymentWrapper( productIdentifier: purchaseParam.productDetails.id, - quantity: 1, + quantity: + purchaseParam is AppStorePurchaseParam ? purchaseParam.quantity : 1, applicationUsername: purchaseParam.applicationUserName, simulatesAskToBuyInSandbox: purchaseParam is AppStorePurchaseParam && purchaseParam.simulatesAskToBuyInSandbox, - requestData: null)); + paymentDiscount: purchaseParam is AppStorePurchaseParam + ? purchaseParam.discount + : null)); return true; // There's no error feedback from iOS here to return. } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform_addition.dart index 87655df53d34..070c138b32e6 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform_addition.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; -import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; +import '../in_app_purchase_storekit.dart'; import '../store_kit_wrappers.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.dart index 1c2bee5a069a..1fdbfebd6ec5 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.dart @@ -117,3 +117,27 @@ class _SerializedEnums { late SKSubscriptionPeriodUnit unit; late SKProductDiscountPaymentMode discountPaymentMode; } + +/// Serializer for [SKProductDiscountType]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SKProductDiscountTypeConverter()`. +class SKProductDiscountTypeConverter + implements JsonConverter { + /// Default const constructor. + const SKProductDiscountTypeConverter(); + + @override + SKProductDiscountType fromJson(int? json) { + if (json == null) { + return SKProductDiscountType.introductory; + } + return $enumDecode( + _$SKProductDiscountTypeEnumMap.cast(), + json); + } + + @override + int toJson(SKProductDiscountType object) => + _$SKProductDiscountTypeEnumMap[object]!; +} diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.g.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.g.dart index 0d05720dc7ae..dc6c17276c1c 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.g.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.g.dart @@ -35,3 +35,8 @@ const _$SKProductDiscountPaymentModeEnumMap = { SKProductDiscountPaymentMode.freeTrail: 2, SKProductDiscountPaymentMode.unspecified: -1, }; + +const _$SKProductDiscountTypeEnumMap = { + SKProductDiscountType.introductory: 0, + SKProductDiscountType.subscription: 1, +}; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart index eb88953096e6..1d98dd3f4250 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; +import '../../store_kit_wrappers.dart'; /// A wrapper around /// [`SKPaymentQueueDelegate`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc). diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 022848281327..d360a2da3fe5 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -3,19 +3,15 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui' show hashValues; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; import 'package:json_annotation/json_annotation.dart'; +import '../../store_kit_wrappers.dart'; import '../channel.dart'; import '../in_app_purchase_storekit_platform.dart'; -import 'sk_payment_queue_delegate_wrapper.dart'; -import 'sk_payment_transaction_wrappers.dart'; -import 'sk_product_wrapper.dart'; part 'sk_payment_queue_wrapper.g.dart'; @@ -365,7 +361,7 @@ class SKError { } @override - int get hashCode => hashValues( + int get hashCode => Object.hash( code, domain, userInfo, @@ -409,7 +405,8 @@ class SKPaymentWrapper { 'applicationUsername': applicationUsername, 'requestData': requestData, 'quantity': quantity, - 'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox + 'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox, + 'paymentDiscount': paymentDiscount?.toMap(), }; } @@ -482,7 +479,7 @@ class SKPaymentWrapper { } @override - int get hashCode => hashValues(productIdentifier, applicationUsername, + int get hashCode => Object.hash(productIdentifier, applicationUsername, quantity, simulatesAskToBuyInSandbox, requestData); @override @@ -585,5 +582,5 @@ class SKPaymentDiscountWrapper { @override int get hashCode => - hashValues(identifier, keyIdentifier, nonce, signature, timestamp); + Object.hash(identifier, keyIdentifier, nonce, signature, timestamp); } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart index 4c4c91257d9d..3894721a1f80 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -190,8 +188,8 @@ class SKPaymentTransactionWrapper { } @override - int get hashCode => hashValues(payment, transactionState, originalTransaction, - transactionTimeStamp, transactionIdentifier, error); + int get hashCode => Object.hash(payment, transactionState, + originalTransaction, transactionTimeStamp, transactionIdentifier, error); @override String toString() => _$SKPaymentTransactionWrapperToJson(this).toString(); diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.dart index 105d999d8a69..5eace6fda69e 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -64,7 +63,7 @@ class SkProductResponseWrapper { } @override - int get hashCode => hashValues(products, invalidProductIdentifiers); + int get hashCode => Object.hash(products, invalidProductIdentifiers); } /// Dart wrapper around StoreKit's [SKProductPeriodUnit](https://developer.apple.com/documentation/storekit/skproductperiodunit?language=objc). @@ -142,7 +141,7 @@ class SKProductSubscriptionPeriodWrapper { } @override - int get hashCode => hashValues(numberOfUnits, unit); + int get hashCode => Object.hash(numberOfUnits, unit); } /// Dart wrapper around StoreKit's [SKProductDiscountPaymentMode](https://developer.apple.com/documentation/storekit/skproductdiscountpaymentmode?language=objc). @@ -168,6 +167,24 @@ enum SKProductDiscountPaymentMode { unspecified, } +/// Dart wrapper around StoreKit's [SKProductDiscountType] +/// (https://developer.apple.com/documentation/storekit/skproductdiscounttype?language=objc) +/// +/// This is used as a property in the [SKProductDiscountWrapper]. +/// The values of the enum options are matching the [SKProductDiscountType]'s +/// values. +/// +/// Values representing the types of discount offers an app can present. +enum SKProductDiscountType { + /// A constant indicating the discount type is an introductory offer. + @JsonValue(0) + introductory, + + /// A constant indicating the discount type is a promotional offer. + @JsonValue(1) + subscription, +} + /// Dart wrapper around StoreKit's [SKProductDiscount](https://developer.apple.com/documentation/storekit/skproductdiscount?language=objc). /// /// It is used as a property in [SKProductWrapper]. @@ -183,7 +200,9 @@ class SKProductDiscountWrapper { required this.priceLocale, required this.numberOfPeriods, required this.paymentMode, - required this.subscriptionPeriod}); + required this.subscriptionPeriod, + required this.identifier, + required this.type}); /// Constructing an instance from a map from the Objective-C layer. /// @@ -215,6 +234,16 @@ class SKProductDiscountWrapper { /// and their units and duration do not have to be matched. final SKProductSubscriptionPeriodWrapper subscriptionPeriod; + /// A string used to uniquely identify a discount offer for a product. + /// + /// You set up offers and their identifiers in App Store Connect. + @JsonKey(defaultValue: null) + final String? identifier; + + /// Values representing the types of discount offers an app can present. + @SKProductDiscountTypeConverter() + final SKProductDiscountType type; + @override bool operator ==(Object other) { if (identical(other, this)) { @@ -228,12 +257,14 @@ class SKProductDiscountWrapper { other.priceLocale == priceLocale && other.numberOfPeriods == numberOfPeriods && other.paymentMode == paymentMode && - other.subscriptionPeriod == subscriptionPeriod; + other.subscriptionPeriod == subscriptionPeriod && + other.identifier == identifier && + other.type == type; } @override - int get hashCode => hashValues( - price, priceLocale, numberOfPeriods, paymentMode, subscriptionPeriod); + int get hashCode => Object.hash(price, priceLocale, numberOfPeriods, + paymentMode, subscriptionPeriod, identifier, type); } /// Dart wrapper around StoreKit's [SKProduct](https://developer.apple.com/documentation/storekit/skproduct?language=objc). @@ -342,7 +373,7 @@ class SKProductWrapper { } @override - int get hashCode => hashValues( + int get hashCode => Object.hash( productIdentifier, localizedTitle, localizedDescription, @@ -410,5 +441,5 @@ class SKPriceLocaleWrapper { } @override - int get hashCode => hashValues(currencySymbol, currencyCode); + int get hashCode => Object.hash(currencySymbol, currencyCode); } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart index 6eea3ff34da0..9e891e75b497 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart @@ -42,6 +42,9 @@ SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) => (json['subscriptionPeriod'] as Map?)?.map( (k, e) => MapEntry(k as String, e), )), + identifier: json['identifier'] as String? ?? null, + type: + const SKProductDiscountTypeConverter().fromJson(json['type'] as int?), ); SKProductWrapper _$SKProductWrapperFromJson(Map json) => SKProductWrapper( diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart index 0bf1103a5abd..ff9e9b7db746 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -56,7 +54,7 @@ class SKStorefrontWrapper { } @override - int get hashCode => hashValues( + int get hashCode => Object.hash( countryCode, identifier, ); 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 b2d8eea9d791..0e7e24166c4d 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 @@ -12,7 +12,9 @@ class AppStorePurchaseParam extends PurchaseParam { AppStorePurchaseParam({ required ProductDetails productDetails, String? applicationUserName, + this.quantity = 1, this.simulatesAskToBuyInSandbox = false, + this.discount, }) : super( productDetails: productDetails, applicationUserName: applicationUserName, @@ -28,4 +30,10 @@ class AppStorePurchaseParam extends PurchaseParam { /// /// See also [SKPaymentWrapper.simulatesAskToBuyInSandbox]. final bool simulatesAskToBuyInSandbox; + + /// Quantity of the product user requested to buy. + final int quantity; + + /// Discount applied to the product. The value is `null` when the product does not have a discount. + final SKPaymentDiscountWrapper? discount; } 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 7e701686e00d..0b6e21a26978 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 platform 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.0+2 +version: 0.3.3 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: 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 9667d789f1f7..e6b9696c8cb1 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 @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:io'; import 'package:flutter/services.dart'; @@ -31,6 +30,7 @@ class FakeStoreKitPlatform { PlatformException? restoreException; SKError? testRestoredError; bool queueIsActive = false; + Map discountReceived = {}; void reset() { transactions = []; @@ -55,65 +55,68 @@ class FakeStoreKitPlatform { restoreException = null; testRestoredError = null; queueIsActive = false; + discountReceived = {}; } - SKPaymentTransactionWrapper createPendingTransaction(String id) { + SKPaymentTransactionWrapper createPendingTransaction(String id, + {int quantity = 1}) { return SKPaymentTransactionWrapper( - transactionIdentifier: '', - payment: SKPaymentWrapper(productIdentifier: id), - transactionState: SKPaymentTransactionStateWrapper.purchasing, - transactionTimeStamp: 123123.121, - error: null, - originalTransaction: null); + transactionIdentifier: '', + payment: SKPaymentWrapper(productIdentifier: id, quantity: quantity), + transactionState: SKPaymentTransactionStateWrapper.purchasing, + transactionTimeStamp: 123123.121, + ); } SKPaymentTransactionWrapper createPurchasedTransaction( - String productId, String transactionId) { + String productId, String transactionId, + {int quantity = 1}) { return SKPaymentTransactionWrapper( - payment: SKPaymentWrapper(productIdentifier: productId), + payment: + SKPaymentWrapper(productIdentifier: productId, quantity: quantity), transactionState: SKPaymentTransactionStateWrapper.purchased, transactionTimeStamp: 123123.121, - transactionIdentifier: transactionId, - error: null, - originalTransaction: null); + transactionIdentifier: transactionId); } - SKPaymentTransactionWrapper createFailedTransaction(String productId) { + SKPaymentTransactionWrapper createFailedTransaction(String productId, + {int quantity = 1}) { return SKPaymentTransactionWrapper( transactionIdentifier: '', - payment: SKPaymentWrapper(productIdentifier: productId), + payment: + SKPaymentWrapper(productIdentifier: productId, quantity: quantity), transactionState: SKPaymentTransactionStateWrapper.failed, transactionTimeStamp: 123123.121, error: const SKError( code: 0, domain: 'ios_domain', - userInfo: {'message': 'an error message'}), - originalTransaction: null); + userInfo: {'message': 'an error message'})); } SKPaymentTransactionWrapper createCanceledTransaction( - String productId, int errorCode) { + String productId, int errorCode, + {int quantity = 1}) { return SKPaymentTransactionWrapper( transactionIdentifier: '', - payment: SKPaymentWrapper(productIdentifier: productId), + payment: + SKPaymentWrapper(productIdentifier: productId, quantity: quantity), transactionState: SKPaymentTransactionStateWrapper.failed, transactionTimeStamp: 123123.121, error: SKError( code: errorCode, domain: 'ios_domain', - userInfo: const {'message': 'an error message'}), - originalTransaction: null); + userInfo: const {'message': 'an error message'})); } SKPaymentTransactionWrapper createRestoredTransaction( - String productId, String transactionId) { + String productId, String transactionId, + {int quantity = 1}) { return SKPaymentTransactionWrapper( - payment: SKPaymentWrapper(productIdentifier: productId), + payment: + SKPaymentWrapper(productIdentifier: productId, quantity: quantity), transactionState: SKPaymentTransactionStateWrapper.restored, transactionTimeStamp: 123123.121, - transactionIdentifier: transactionId, - error: null, - originalTransaction: null); + transactionIdentifier: transactionId); } Future onMethodCall(MethodCall call) { @@ -167,25 +170,41 @@ class FakeStoreKitPlatform { return Future.sync(() {}); case '-[InAppPurchasePlugin addPayment:result:]': final String id = call.arguments['productIdentifier'] as String; + final int quantity = call.arguments['quantity'] as int; + + // Keep the received paymentDiscount parameter when testing payment with discount. + if (call.arguments['applicationUsername'] == 'userWithDiscount') { + if (call.arguments['paymentDiscount'] != null) { + final Map discountArgument = + call.arguments['paymentDiscount'] as Map; + discountReceived = discountArgument.cast(); + } else { + discountReceived = {}; + } + } + final SKPaymentTransactionWrapper transaction = - createPendingTransaction(id); + createPendingTransaction(id, quantity: quantity); + transactions.add(transaction); InAppPurchaseStoreKitPlatform.observer.updatedTransactions( transactions: [transaction]); sleep(const Duration(milliseconds: 30)); if (testTransactionFail) { final SKPaymentTransactionWrapper transactionFailed = - createFailedTransaction(id); + createFailedTransaction(id, quantity: quantity); InAppPurchaseStoreKitPlatform.observer.updatedTransactions( transactions: [transactionFailed]); } else if (testTransactionCancel > 0) { final SKPaymentTransactionWrapper transactionCanceled = - createCanceledTransaction(id, testTransactionCancel); + createCanceledTransaction(id, testTransactionCancel, + quantity: quantity); InAppPurchaseStoreKitPlatform.observer.updatedTransactions( transactions: [transactionCanceled]); } else { final SKPaymentTransactionWrapper transactionFinished = createPurchasedTransaction( - id, transaction.transactionIdentifier ?? ''); + id, transaction.transactionIdentifier ?? '', + quantity: quantity); InAppPurchaseStoreKitPlatform.observer.updatedTransactions( transactions: [transactionFinished]); } @@ -193,7 +212,8 @@ class FakeStoreKitPlatform { case '-[InAppPurchasePlugin finishTransaction:result:]': finishedTransactions.add(createPurchasedTransaction( call.arguments['productIdentifier'] as String, - call.arguments['transactionIdentifier'] as String)); + call.arguments['transactionIdentifier'] as String, + quantity: transactions.first.payment.quantity)); break; case '-[SKPaymentQueue startObservingTransactionQueue]': queueIsActive = true; 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 e92d8487ed93..51ff2c229483 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 @@ -427,6 +427,100 @@ void main() { final PurchaseStatus purchaseStatus = await completer.future; expect(purchaseStatus, PurchaseStatus.canceled); }); + + test( + 'buying non consumable, should be able to purchase multiple quantity of one product', + () async { + final List details = []; + final Completer> completer = + Completer>(); + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { + details.addAll(purchaseDetailsList); + for (final PurchaseDetails purchaseDetails in purchaseDetailsList) { + if (purchaseDetails.pendingCompletePurchase) { + iapStoreKitPlatform.completePurchase(purchaseDetails); + completer.complete(details); + subscription.cancel(); + } + } + }); + final AppStoreProductDetails productDetails = + AppStoreProductDetails.fromSKProduct(dummyProductWrapper); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: productDetails, + quantity: 5, + applicationUserName: 'appName'); + await iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam); + await completer.future; + expect( + fakeStoreKitPlatform.finishedTransactions.first.payment.quantity, 5); + }); + + test( + 'buying consumable, should be able to purchase multiple quantity of one product', + () async { + final List details = []; + final Completer> completer = + Completer>(); + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { + details.addAll(purchaseDetailsList); + for (final PurchaseDetails purchaseDetails in purchaseDetailsList) { + if (purchaseDetails.pendingCompletePurchase) { + iapStoreKitPlatform.completePurchase(purchaseDetails); + completer.complete(details); + subscription.cancel(); + } + } + }); + final AppStoreProductDetails productDetails = + AppStoreProductDetails.fromSKProduct(dummyProductWrapper); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: productDetails, + quantity: 5, + applicationUserName: 'appName'); + await iapStoreKitPlatform.buyConsumable(purchaseParam: purchaseParam); + await completer.future; + expect( + fakeStoreKitPlatform.finishedTransactions.first.payment.quantity, 5); + }); + + test( + 'buying non consumable with discount, should get purchase objects in the purchase update callback', + () async { + final List details = []; + final Completer> completer = + Completer>(); + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { + details.addAll(purchaseDetailsList); + if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { + completer.complete(details); + subscription.cancel(); + } + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'userWithDiscount', + discount: dummyPaymentDiscountWrapper, + ); + await iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam); + + final List result = await completer.future; + expect(result.length, 2); + expect(result.first.productID, dummyProductWrapper.productIdentifier); + expect(fakeStoreKitPlatform.discountReceived, + dummyPaymentDiscountWrapper.toMap()); + }); }); group('complete purchase', () { 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 2baf20892ab6..ff059ffbb1fc 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 @@ -222,7 +222,7 @@ class FakeStoreKitPlatform { case '-[InAppPurchasePlugin startProductRequest:result:]': startProductRequestParam = call.arguments as List; if (getProductRequestFailTest) { - return Future.value(null); + return Future.value(); } return Future>.value( buildProductResponseMap(dummyProductResponseWrapper)); @@ -240,7 +240,7 @@ class FakeStoreKitPlatform { // payment queue case '-[SKPaymentQueue canMakePayments:]': if (testReturnNull) { - return Future.value(null); + return Future.value(); } return Future.value(true); case '-[SKPaymentQueue transactions]': diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart index fdf80d68a3ea..b6de5e035c5e 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:in_app_purchase_storekit/src/store_kit_wrappers/sk_product_wrapper.dart'; import 'package:in_app_purchase_storekit/src/types/app_store_product_details.dart'; import 'package:in_app_purchase_storekit/src/types/app_store_purchase_details.dart'; import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; @@ -17,7 +16,7 @@ void main() { () { final SKProductSubscriptionPeriodWrapper wrapper = SKProductSubscriptionPeriodWrapper.fromJson( - buildSubscriptionPeriodMap(dummySubscription)!); + buildSubscriptionPeriodMap(dummySubscription)); expect(wrapper, equals(dummySubscription)); }); @@ -39,6 +38,16 @@ void main() { expect(wrapper, equals(dummyDiscount)); }); + test( + 'SKProductDiscountWrapper missing identifier and type should have ' + 'property values consistent with map', () { + final SKProductDiscountWrapper wrapper = + SKProductDiscountWrapper.fromJson( + buildDiscountMapMissingIdentifierAndType( + dummyDiscountMissingIdentifierAndType)); + expect(wrapper, equals(dummyDiscountMissingIdentifierAndType)); + }); + test( 'SKProductDiscountWrapper should have properties to be default if map is empty', () { @@ -96,8 +105,7 @@ void main() { expect(product.title, wrapper.localizedTitle); expect(product.description, wrapper.localizedDescription); expect(product.id, wrapper.productIdentifier); - expect(product.price, - wrapper.priceLocale.currencySymbol + wrapper.price.toString()); + expect(product.price, wrapper.priceLocale.currencySymbol + wrapper.price); expect(product.skProduct, wrapper); }); @@ -133,6 +141,21 @@ void main() { expect(payment, equals(dummyPayment)); }); + test('SKPaymentWrapper should have propery values consistent with .toMap()', + () { + final Map mapResult = dummyPaymentWithDiscount.toMap(); + expect(mapResult['productIdentifier'], + dummyPaymentWithDiscount.productIdentifier); + expect(mapResult['applicationUsername'], + dummyPaymentWithDiscount.applicationUsername); + expect(mapResult['requestData'], dummyPaymentWithDiscount.requestData); + expect(mapResult['quantity'], dummyPaymentWithDiscount.quantity); + expect(mapResult['simulatesAskToBuyInSandbox'], + dummyPaymentWithDiscount.simulatesAskToBuyInSandbox); + expect(mapResult['paymentDiscount'], + equals(dummyPaymentWithDiscount.paymentDiscount?.toMap())); + }); + test('Should construct correct SKError from json', () { final SKError error = SKError.fromJson(buildErrorMap(dummyError)); expect(error, equals(dummyError)); diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart index 51d851cb79b5..6601a21c4ee4 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart @@ -10,6 +10,15 @@ const SKPaymentWrapper dummyPayment = SKPaymentWrapper( requestData: 'fake-data-utf8', quantity: 2, simulatesAskToBuyInSandbox: true); + +final SKPaymentWrapper dummyPaymentWithDiscount = SKPaymentWrapper( + productIdentifier: 'prod-id', + applicationUsername: 'app-user-name', + requestData: 'fake-data-utf8', + quantity: 2, + simulatesAskToBuyInSandbox: true, + paymentDiscount: dummyPaymentDiscountWrapper); + const SKError dummyError = SKError( code: 111, domain: 'dummy-domain', @@ -19,7 +28,6 @@ final SKPaymentTransactionWrapper dummyOriginalTransaction = SKPaymentTransactionWrapper( transactionState: SKPaymentTransactionStateWrapper.purchased, payment: dummyPayment, - originalTransaction: null, transactionTimeStamp: 1231231231.00, transactionIdentifier: '123123', error: dummyError, @@ -59,6 +67,19 @@ final SKProductDiscountWrapper dummyDiscount = SKProductDiscountWrapper( numberOfPeriods: 1, paymentMode: SKProductDiscountPaymentMode.payUpFront, subscriptionPeriod: dummySubscription, + identifier: 'id', + type: SKProductDiscountType.subscription, +); + +final SKProductDiscountWrapper dummyDiscountMissingIdentifierAndType = + SKProductDiscountWrapper( + price: '1.0', + priceLocale: dollarLocale, + numberOfPeriods: 1, + paymentMode: SKProductDiscountPaymentMode.payUpFront, + subscriptionPeriod: dummySubscription, + identifier: null, + type: SKProductDiscountType.introductory, ); final SKProductWrapper dummyProductWrapper = SKProductWrapper( @@ -107,6 +128,21 @@ Map buildDiscountMap(SKProductDiscountWrapper discount) { SKProductDiscountPaymentMode.values.indexOf(discount.paymentMode), 'subscriptionPeriod': buildSubscriptionPeriodMap(discount.subscriptionPeriod), + 'identifier': discount.identifier, + 'type': SKProductDiscountType.values.indexOf(discount.type) + }; +} + +Map buildDiscountMapMissingIdentifierAndType( + SKProductDiscountWrapper discount) { + return { + 'price': discount.price, + 'priceLocale': buildLocaleMap(discount.priceLocale), + 'numberOfPeriods': discount.numberOfPeriods, + 'paymentMode': + SKProductDiscountPaymentMode.values.indexOf(discount.paymentMode), + 'subscriptionPeriod': + buildSubscriptionPeriodMap(discount.subscriptionPeriod) }; } @@ -159,3 +195,12 @@ Map buildTransactionMap( }; return map; } + +final SKPaymentDiscountWrapper dummyPaymentDiscountWrapper = + SKPaymentDiscountWrapper.fromJson(const { + 'identifier': 'dummy-discount-identifier', + 'keyIdentifier': 'KEYIDTEST1', + 'nonce': '00000000-0000-0000-0000-000000000000', + 'signature': 'dummy-signature-string', + 'timestamp': 1231231231, +}); diff --git a/packages/integration_test/README.md b/packages/integration_test/README.md index 6bf388131680..83c4adb500f0 100644 --- a/packages/integration_test/README.md +++ b/packages/integration_test/README.md @@ -16,256 +16,3 @@ dev_dependencies: For the latest documentation, see [Integration testing](https://flutter.dev/docs/testing/integration-tests). - -## Old instructions - -This package enables self-driving testing of Flutter code on devices and emulators. -It adapts flutter_test results into a format that is compatible with `flutter drive` -and native Android instrumentation testing. - -## Usage - -Add a dependency on the `integration_test` and `flutter_test` package in the -`dev_dependencies` section of `pubspec.yaml`. For plugins, do this in the -`pubspec.yaml` of the example app. - -Create a `integration_test/` directory for your package. In this directory, -create a `_test.dart`, using the following as a starting point to make -assertions. - -Note: You should only use `testWidgets` to declare your tests, or errors will not be reported correctly. - -```dart -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets("failing test example", (WidgetTester tester) async { - expect(2 + 2, equals(5)); - }); -} -``` - -### Driver Entrypoint - -An accompanying driver script will be needed that can be shared across all -integration tests. Create a file named `integration_test.dart` in the -`test_driver/` directory with the following contents: - -```dart -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); -``` - -You can also use different driver scripts to customize the behavior of the app -under test. For example, `FlutterDriver` can also be parameterized with -different [options](https://api.flutter.dev/flutter/flutter_driver/FlutterDriver/connect.html). -See the [extended driver](https://github.com/flutter/flutter/blob/master/packages/integration_test/example/test_driver/extended_integration_test.dart) for an example. - -### Package Structure - -Your package should have a structure that looks like this: - -``` -lib/ - ... -integration_test/ - foo_test.dart - bar_test.dart -test/ - # Other unit tests go here. -test_driver/ - integration_test.dart -``` - -[Example](https://github.com/flutter/plugins/tree/master/packages/integration_test/example) - -## Using Flutter Driver to Run Tests - -These tests can be launched with the `flutter drive` command. - -To run the `integration_test/foo_test.dart` test with the -`test_driver/integration_test.dart` driver, use the following command: - -```sh -flutter drive \ - --driver=test_driver/integration_test.dart \ - --target=integration_test/foo_test.dart -``` - -### Web - -Make sure you have [enabled web support](https://flutter.dev/docs/get-started/web#set-up) -then [download and run](https://flutter.dev/docs/cookbook/testing/integration/introduction#6b-web) -the web driver in another process. - -Use following command to execute the tests: - -```sh -flutter drive \ - --driver=test_driver/integration_test.dart \ - --target=integration_test/foo_test.dart \ - -d web-server -``` - -## Android Device Testing - -Create an instrumentation test file in your application's -**android/app/src/androidTest/java/com/example/myapp/** directory (replacing -com, example, and myapp with values from your app's package name). You can name -this test file `MainActivityTest.java` or another name of your choice. - -```java -package com.example.myapp; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class MainActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class, true, false); -} -``` - -Update your application's **myapp/android/app/build.gradle** to make sure it -uses androidx's version of `AndroidJUnitRunner` and has androidx libraries as a -dependency. - -```gradle -android { - ... - defaultConfig { - ... - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } -} - -dependencies { - testImplementation 'junit:junit:4.12' - - // https://developer.android.com/jetpack/androidx/releases/test/#1.2.0 - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' -} -``` - -To run `integration_test/foo_test.dart` on a local Android device (emulated or -physical): - -```sh -./gradlew app:connectedAndroidTest -Ptarget=`pwd`/../integration_test/foo_test.dart -``` - -## Firebase Test Lab - -If this is your first time testing with Firebase Test Lab, you'll need to follow -the guides in the [Firebase test lab -documentation](https://firebase.google.com/docs/test-lab/?gclid=EAIaIQobChMIs5qVwqW25QIV8iCtBh3DrwyUEAAYASAAEgLFU_D_BwE) -to set up a project. - -To run a test on Android devices using Firebase Test Lab, use gradle commands to build an -instrumentation test for Android, after creating `androidTest` as suggested in the last section. - -```bash -pushd android -# flutter build generates files in android/ for building the app -flutter build apk -./gradlew app:assembleAndroidTest -./gradlew app:assembleDebug -Ptarget=.dart -popd -``` - -Upload the build apks Firebase Test Lab, making sure to replace , -, , and with your values. - -```bash -gcloud auth activate-service-account --key-file= -gcloud --quiet config set project -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 2m \ - --results-bucket= \ - --results-dir= -``` - -You can pass additional parameters on the command line, such as the -devices you want to test on. See -[gcloud firebase test android run](https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run). - -## iOS Device Testing - -Open `ios/Runner.xcworkspace` in Xcode. Create a test target if you -do not already have one via `File > New > Target...` and select `Unit Testing Bundle`. -Change the `Product Name` to `RunnerTests`. Make sure `Target to be Tested` is set to `Runner` and language is set to `Objective-C`. -Select `Finish`. -Make sure that the **iOS Deployment Target** of `RunnerTests` within the **Build Settings** section is the same as `Runner`. - -Add the new test target to `ios/Podfile` by embedding in the existing `Runner` target. - -```ruby -target 'Runner' do - # Do not change existing lines. - ... - - target 'RunnerTests' do - inherit! :search_paths - end -end -``` - -To build `integration_test/foo_test.dart` from the command line, run: -```sh -flutter build ios --config-only integration_test/foo_test.dart -``` - -In Xcode, add a test file called `RunnerTests.m` (or any name of your choice) to the new target and -replace the file: - -```objective-c -@import XCTest; -@import integration_test; - -INTEGRATION_TEST_IOS_RUNNER(RunnerTests) -``` - -Run `Product > Test` to run the integration tests on your selected device. - -To deploy it to Firebase Test Lab you can follow these steps: - -Execute this script at the root of your Flutter app: - -```sh -output="../build/ios_integ" -product="build/ios_integ/Build/Products" -dev_target="14.3" - -# Pass --simulator if building for the simulator. -flutter build ios integration_test/foo_test.dart --release - -pushd ios -xcodebuild -workspace Runner.xcworkspace -scheme Runner -config Flutter/Release.xcconfig -derivedDataPath $output -sdk iphoneos build-for-testing -popd - -pushd $product -zip -r "ios_tests.zip" "Release-iphoneos" "Runner_iphoneos$dev_target-arm64.xctestrun" -popd -``` - -You can verify locally that your tests are successful by running the following command: - -```sh -xcodebuild test-without-building -xctestrun "build/ios_integ/Build/Products/Runner_iphoneos14.3-arm64.xctestrun" -destination id= -``` - -Once everything is ok, you can upload the resulting zip to Firebase Test Lab (change the model with your values): - -```sh -gcloud firebase test ios run --test "build/ios_integ/ios_tests.zip" --device model=iphone11pro,version=14.1,locale=fr_FR,orientation=portrait -``` diff --git a/packages/ios_platform_images/CHANGELOG.md b/packages/ios_platform_images/CHANGELOG.md index 416f93661c8d..1a28c9b2a550 100644 --- a/packages/ios_platform_images/CHANGELOG.md +++ b/packages/ios_platform_images/CHANGELOG.md @@ -1,3 +1,31 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). + +## 0.2.0+9 + +* Ignores the warning for the upcoming deprecation of `DecoderCallback`. + +## 0.2.0+8 + +* Ignores the warning for the upcoming deprecation of `ImageProvider.load` in the correct line. + +## 0.2.0+7 + +* Ignores the warning for the upcoming deprecation of `ImageProvider.load`. + +## 0.2.0+6 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.2.0+5 + +* Migrates from `ui.hash*` to `Object.hash*`. +* Adds OS version support information to README. + ## 0.2.0+4 * Internal code cleanup for stricter analysis options. diff --git a/packages/ios_platform_images/README.md b/packages/ios_platform_images/README.md index ada89fcdffec..08dfc3e40b31 100644 --- a/packages/ios_platform_images/README.md +++ b/packages/ios_platform_images/README.md @@ -8,6 +8,10 @@ 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+ | + ## Usage ### iOS->Flutter Example diff --git a/packages/ios_platform_images/example/README.md b/packages/ios_platform_images/example/README.md index 2f34abc202f2..91fc3baf5f49 100644 --- a/packages/ios_platform_images/example/README.md +++ b/packages/ios_platform_images/example/README.md @@ -1,16 +1,3 @@ # ios_platform_images_example Demonstrates how to use the ios_platform_images plugin. - -## 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://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/packages/ios_platform_images/example/lib/main.dart b/packages/ios_platform_images/example/lib/main.dart index ecdfeb2cba01..929814ecce00 100644 --- a/packages/ios_platform_images/example/lib/main.dart +++ b/packages/ios_platform_images/example/lib/main.dart @@ -5,12 +5,15 @@ import 'package:flutter/material.dart'; import 'package:ios_platform_images/ios_platform_images.dart'; -void main() => runApp(MyApp()); +void main() => runApp(const MyApp()); /// Main widget for the example app. class MyApp extends StatefulWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { diff --git a/packages/ios_platform_images/example/pubspec.yaml b/packages/ios_platform_images/example/pubspec.yaml index aa8fea54b287..6045b3f67cfc 100644 --- a/packages/ios_platform_images/example/pubspec.yaml +++ b/packages/ios_platform_images/example/pubspec.yaml @@ -4,16 +4,12 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" dependencies: cupertino_icons: ^1.0.2 flutter: sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter ios_platform_images: # When depending on this package from a real application you should use: # ios_platform_images: ^x.y.z @@ -22,5 +18,9 @@ dev_dependencies: # the parent directory to use the current plugin's version. path: ../ +dev_dependencies: + flutter_test: + sdk: flutter + flutter: uses-material-design: true diff --git a/packages/ios_platform_images/example/test/widget_test.dart b/packages/ios_platform_images/example/test/widget_test.dart index 18e9e657ddb9..f3cd4c68b65b 100644 --- a/packages/ios_platform_images/example/test/widget_test.dart +++ b/packages/ios_platform_images/example/test/widget_test.dart @@ -12,7 +12,7 @@ import 'package:ios_platform_images_example/main.dart'; void main() { testWidgets('Verify loads image', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); + await tester.pumpWidget(const MyApp()); expect( find.byWidgetPredicate( diff --git a/packages/ios_platform_images/lib/ios_platform_images.dart b/packages/ios_platform_images/lib/ios_platform_images.dart index 23a437d775ef..fa40eb08fafd 100644 --- a/packages/ios_platform_images/lib/ios_platform_images.dart +++ b/packages/ios_platform_images/lib/ios_platform_images.dart @@ -3,12 +3,13 @@ // found in the LICENSE file. import 'dart:async'; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart' show SynchronousFuture, describeIdentity, immutable, objectRuntimeType; -import 'package:flutter/painting.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -63,8 +64,11 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { return SynchronousFuture<_FutureMemoryImage>(this); } + // ignore:deprecated_member_use /// See [ImageProvider.load]. + // TODO(jmagman): Implement the new API once it lands, https://github.com/flutter/flutter/issues/103556 @override + // ignore: deprecated_member_use ImageStreamCompleter load(_FutureMemoryImage key, DecoderCallback decode) { return _FutureImageStreamCompleter( codec: _loadAsync(key, decode), @@ -74,6 +78,7 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { Future _loadAsync( _FutureMemoryImage key, + // ignore: deprecated_member_use DecoderCallback decode, ) async { assert(key == this); @@ -95,7 +100,7 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { /// See [ImageProvider.hashCode]. @override - int get hashCode => hashValues(_futureBytes.hashCode, _futureScale); + int get hashCode => Object.hash(_futureBytes.hashCode, _futureScale); /// See [ImageProvider.toString]. @override diff --git a/packages/ios_platform_images/pubspec.yaml b/packages/ios_platform_images/pubspec.yaml index fe7952abbc3b..8b32b39343a7 100644 --- a/packages/ios_platform_images/pubspec.yaml +++ b/packages/ios_platform_images/pubspec.yaml @@ -2,11 +2,11 @@ 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.0+4 +version: 0.2.0+9 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: @@ -21,4 +21,3 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/local_auth/local_auth/AUTHORS b/packages/local_auth/local_auth/AUTHORS index 493a0b4ef9c2..d5694690c247 100644 --- a/packages/local_auth/local_auth/AUTHORS +++ b/packages/local_auth/local_auth/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Bodhi Mulders diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index 9387540d6795..34e26efef238 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -1,3 +1,71 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 2.1.2 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 2.1.1 + +* Replaces `USE_FINGERPRINT` permission with `USE_BIOMETRIC` in README and example project. + +## 2.1.0 + +* Adds Windows support. + +## 2.0.2 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 2.0.1 + +* Restores the ability to import `error_codes.dart`. +* Updates README to match API changes in 2.0, and to improve clarity in + general. +* Removes unnecessary imports. + +## 2.0.0 + +* Migrates plugin to federated architecture. +* Adds OS version support information to README. +* BREAKING CHANGE: Deprecated method `authenticateWithBiometrics` has been removed. + Use `authenticate` instead. +* BREAKING CHANGE: Enum `BiometricType` has been expanded with options for `strong` and `weak`, + and applications should be updated to handle these accordingly. +* BREAKING CHANGE: Parameters of `authenticate` have been changed. + + Example: + ```dart + // Old way of calling `authenticate`. + Future authenticate( + localizedReason: 'localized reason', + useErrorDialogs: true, + stickyAuth: false, + androidAuthStrings: const AndroidAuthMessages(), + iOSAuthStrings: const IOSAuthMessages(), + sensitiveTransaction: true, + biometricOnly: false, + ); + // New way of calling `authenticate`. + Future authenticate( + localizedReason: 'localized reason', + authMessages: const [ + IOSAuthMessages(), + AndroidAuthMessages() + ], + options: const AuthenticationOptions( + useErrorDialogs: true, + stickyAuth: false, + sensitiveTransaction: true, + biometricOnly: false, + ), + ); + ``` + + + ## 1.1.11 * Adds support `localizedFallbackTitle` in authenticateWithBiometrics on iOS. diff --git a/packages/local_auth/local_auth/README.md b/packages/local_auth/local_auth/README.md index 84470c646e6b..a68692ea940a 100644 --- a/packages/local_auth/local_auth/README.md +++ b/packages/local_auth/local_auth/README.md @@ -1,137 +1,188 @@ # local_auth + + This Flutter plugin provides means to perform local, on-device authentication of the user. -This means referring to biometric authentication on iOS (Touch ID or lock code) -and the fingerprint APIs on Android (introduced in Android 6.0). +On supported devices, this includes authentication with biometrics such as +fingerprint or facial recognition. -## Usage in Dart +| | Android | iOS | Windows | +|-------------|-----------|------|-------------| +| **Support** | SDK 16+\* | 9.0+ | Windows 10+ | -Import the relevant file: +## Usage -```dart -import 'package:local_auth/local_auth.dart'; -``` +### Device Capabilities -To check whether there is local authentication available on this device or not, call canCheckBiometrics: +To check whether there is local authentication available on this device or not, +call `canCheckBiometrics` (if you need biometrics support) and/or +`isDeviceSupported()` (if you just need some device-level authentication): + ```dart -bool canCheckBiometrics = - await localAuth.canCheckBiometrics; +import 'package:local_auth/local_auth.dart'; +// ··· + final LocalAuthentication auth = LocalAuthentication(); + // ··· + final bool canAuthenticateWithBiometrics = await auth.canCheckBiometrics; + final bool canAuthenticate = + canAuthenticateWithBiometrics || await auth.isDeviceSupported(); ``` Currently the following biometric types are implemented: - BiometricType.face - BiometricType.fingerprint +- BiometricType.weak +- BiometricType.strong + +### Enrolled Biometrics + +`canCheckBiometrics` only indicates whether hardware support is available, not +whether the device has any biometrics enrolled. To get a list of enrolled +biometrics, call `getAvailableBiometrics()`. -To get a list of enrolled biometrics, call getAvailableBiometrics: +The types are device-specific and platform-specific, and other types may be +added in the future, so when possible you should not rely on specific biometric +types and only check that some biometric is enrolled: + ```dart -List availableBiometrics = +final List availableBiometrics = await auth.getAvailableBiometrics(); -if (Platform.isIOS) { - if (availableBiometrics.contains(BiometricType.face)) { - // Face ID. - } else if (availableBiometrics.contains(BiometricType.fingerprint)) { - // Touch ID. - } +if (availableBiometrics.isNotEmpty) { + // Some biometrics are enrolled. } -``` -We have default dialogs with an 'OK' button to show authentication error -messages for the following 2 cases: - -1. Passcode/PIN/Pattern Not Set. The user has not yet configured a passcode on - iOS or PIN/pattern on Android. -2. Touch ID/Fingerprint Not Enrolled. The user has not enrolled any - fingerprints on the device. - -Which means, if there's no fingerprint on the user's device, a dialog with -instructions will pop up to let the user set up fingerprint. If the user clicks -'OK' button, it will return 'false'. +if (availableBiometrics.contains(BiometricType.strong) || + availableBiometrics.contains(BiometricType.face)) { + // Specific types of biometrics are available. + // Use checks like this with caution! +} +``` -Use the exported APIs to trigger local authentication with default dialogs: +### Options -The `authenticate()` method uses biometric authentication, but also allows -users to use pin, pattern, or passcode. +The `authenticate()` method uses biometric authentication when possible, but +also allows fallback to pin, pattern, or passcode. + ```dart -var localAuth = LocalAuthentication(); -bool didAuthenticate = - await localAuth.authenticate( - localizedReason: 'Please authenticate to show account balance'); +try { + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance'); + // ··· +} on PlatformException { + // ... +} ``` -To authenticate using biometric authentication only, set `biometricOnly` to `true`. +To require biometric authentication, pass `AuthenticationOptions` with +`biometricOnly` set to `true`. + ```dart -var localAuth = LocalAuthentication(); -bool didAuthenticate = - await localAuth.authenticate( - localizedReason: 'Please authenticate to show account balance', - biometricOnly: true); +final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance', + options: const AuthenticationOptions(biometricOnly: true)); ``` -If you don't want to use the default dialogs, call this API with -'useErrorDialogs = false'. In this case, it will throw the error message back -and you need to handle them in your dart code: +*Note*: `biometricOnly` is not supported on Windows since the Windows implementation's underlying API (Windows Hello) doesn't support selecting the authentication method. -```dart -bool didAuthenticate = - await localAuth.authenticate( - localizedReason: 'Please authenticate to show account balance', - useErrorDialogs: false); -``` +#### Dialogs -You can use our default dialog messages, or you can use your own messages by -passing in IOSAuthMessages and AndroidAuthMessages: +The plugin provides default dialogs for the following cases: -```dart -import 'package:local_auth/auth_strings.dart'; - -const iosStrings = const IOSAuthMessages( - cancelButton: 'cancel', - goToSettingsButton: 'settings', - goToSettingsDescription: 'Please set up your Touch ID.', - lockOut: 'Please reenable your Touch ID'); -await localAuth.authenticate( - localizedReason: 'Please authenticate to show account balance', - useErrorDialogs: false, - iOSAuthStrings: iosStrings); +1. Passcode/PIN/Pattern Not Set: The user has not yet configured a passcode on + iOS or PIN/pattern on Android. +2. Biometrics Not Enrolled: The user has not enrolled any biometrics on the + device. -``` +If a user does not have the necessary authentication enrolled when +`authenticate` is called, they will be given the option to enroll at that point, +or cancel authentication. -If needed, you can manually stop authentication for android: +If you don't want to use the default dialogs, set the `useErrorDialogs` option +to `false` to have `authenticate` immediately return an error in those cases. + ```dart +import 'package:local_auth/error_codes.dart' as auth_error; +// ··· + try { + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance', + options: const AuthenticationOptions(useErrorDialogs: false)); + // ··· + } on PlatformException catch (e) { + if (e.code == auth_error.notAvailable) { + // Add handling of no hardware here. + } else if (e.code == auth_error.notEnrolled) { + // ... + } else { + // ... + } + } +``` -void _cancelAuthentication() { - localAuth.stopAuthentication(); -} +If you want to customize the messages in the dialogs, you can pass +`AuthMessages` for each platform you support. These are platform-specific, so +you will need to import the platform-specific implementation packages. For +instance, to customize Android and iOS: + +```dart +import 'package:local_auth_android/local_auth_android.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; +// ··· + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance', + authMessages: const [ + AndroidAuthMessages( + signInTitle: 'Oops! Biometric authentication required!', + cancelButton: 'No thanks', + ), + IOSAuthMessages( + cancelButton: 'No thanks', + ), + ]); ``` +See the platform-specific classes for details about what can be customized on +each platform. + ### Exceptions -There are 6 types of exceptions: PasscodeNotSet, NotEnrolled, NotAvailable, OtherOperatingSystem, LockedOut and PermanentlyLockedOut. -They are wrapped in LocalAuthenticationError class. You can -catch the exception and handle them by different types. For example: +`authenticate` throws `PlatformException`s in many error cases. See +`error_codes.dart` for known error codes that you may want to have specific +handling for. For example: + ```dart import 'package:flutter/services.dart'; import 'package:local_auth/error_codes.dart' as auth_error; - -try { - bool didAuthenticate = await local_auth.authenticate( - localizedReason: 'Please authenticate to show account balance'); -} on PlatformException catch (e) { - if (e.code == auth_error.notAvailable) { - // Handle this exception here. - } -} +import 'package:local_auth/local_auth.dart'; +// ··· + final LocalAuthentication auth = LocalAuthentication(); + // ··· + try { + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance', + options: const AuthenticationOptions(useErrorDialogs: false)); + // ··· + } on PlatformException catch (e) { + if (e.code == auth_error.notEnrolled) { + // Add handling of no hardware here. + } else if (e.code == auth_error.lockedOut || + e.code == auth_error.permanentlyLockedOut) { + // ... + } else { + // ... + } + } ``` ## iOS Integration @@ -149,57 +200,52 @@ app has not been updated to use Face ID. ## Android Integration -Note that local_auth plugin requires the use of a FragmentActivity as -opposed to Activity. This can be easily done by switching to use -`FlutterFragmentActivity` as opposed to `FlutterActivity` in your -manifest (or your own Activity class if you are extending the base class). - -Update your MainActivity.java: - -```java -import android.os.Bundle; -import io.flutter.app.FlutterFragmentActivity; -import io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin; -import io.flutter.plugins.localauth.LocalAuthPlugin; - -public class MainActivity extends FlutterFragmentActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - FlutterAndroidLifecyclePlugin.registerWith( - registrarFor( - "io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin")); - LocalAuthPlugin.registerWith(registrarFor("io.flutter.plugins.localauth.LocalAuthPlugin")); - } -} -``` +\* The plugin will build and run on SDK 16+, but `isDeviceSupported()` will +always return false before SDK 23 (Android 6.0). -OR +### Activity Changes -Update your MainActivity.kt: +Note that `local_auth` requires the use of a `FragmentActivity` instead of an +`Activity`. To update your application: -```kotlin -import io.flutter.embedding.android.FlutterFragmentActivity -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugins.GeneratedPluginRegistrant +* If you are using `FlutterActivity` directly, change it to +`FlutterFragmentActivity` in your `AndroidManifest.xml`. +* If you are using a custom activity, update your `MainActivity.java`: -class MainActivity: FlutterFragmentActivity() { - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - GeneratedPluginRegistrant.registerWith(flutterEngine) + ```java + import io.flutter.embedding.android.FlutterFragmentActivity; + + public class MainActivity extends FlutterFragmentActivity { + // ... } -} -``` + ``` + + or MainActivity.kt: + + ```kotlin + import io.flutter.embedding.android.FlutterFragmentActivity + + class MainActivity: FlutterFragmentActivity() { + // ... + } + ``` + + to inherit from `FlutterFragmentActivity`. + +### Permissions Update your project's `AndroidManifest.xml` file to include the -`USE_FINGERPRINT` permissions: +`USE_BIOMETRIC` permissions: ```xml - + ``` +### Compatibility + On Android, you can check only for existence of fingerprint hardware prior to API 29 (Android Q). Therefore, if you would like to support other biometrics types (such as face scanning) and you want to support SDKs lower than Q, @@ -214,10 +260,3 @@ if the user receives a phone call before they get a chance to authenticate. With `stickyAuth` set to false, this would result in plugin returning failure result to the Dart app. If set to true, the plugin will retry authenticating when the app resumes. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). - -For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/local_auth/local_auth/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java b/packages/local_auth/local_auth/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java deleted file mode 100644 index 41868e603ad8..000000000000 --- a/packages/local_auth/local_auth/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java +++ /dev/null @@ -1,64 +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. - -package io.flutter.plugins.localauth; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.Activity; -import android.content.Context; -import androidx.lifecycle.Lifecycle; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.embedding.engine.dart.DartExecutor; -import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import org.junit.Test; - -public class LocalAuthTest { - @Test - public void isDeviceSupportedReturnsFalse() { - final LocalAuthPlugin plugin = new LocalAuthPlugin(); - final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - plugin.onMethodCall(new MethodCall("isDeviceSupported", null), mockResult); - verify(mockResult).success(false); - } - - @Test - public void onDetachedFromActivity_ShouldReleaseActivity() { - final Activity mockActivity = mock(Activity.class); - final ActivityPluginBinding mockActivityBinding = mock(ActivityPluginBinding.class); - when(mockActivityBinding.getActivity()).thenReturn(mockActivity); - - Context mockContext = mock(Context.class); - when(mockActivity.getBaseContext()).thenReturn(mockContext); - - final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class); - when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference); - - final Lifecycle mockLifecycle = mock(Lifecycle.class); - when(mockLifecycleReference.getLifecycle()).thenReturn(mockLifecycle); - - final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); - final FlutterEngine mockFlutterEngine = mock(FlutterEngine.class); - when(mockPluginBinding.getFlutterEngine()).thenReturn(mockFlutterEngine); - - DartExecutor mockDartExecutor = mock(DartExecutor.class); - when(mockFlutterEngine.getDartExecutor()).thenReturn(mockDartExecutor); - - final LocalAuthPlugin plugin = new LocalAuthPlugin(); - plugin.onAttachedToEngine(mockPluginBinding); - plugin.onAttachedToActivity(mockActivityBinding); - assertNotNull(plugin.getActivity()); - - plugin.onDetachedFromActivity(); - assertNull(plugin.getActivity()); - } -} diff --git a/packages/local_auth/local_auth/example/README.md b/packages/local_auth/local_auth/example/README.md index a4a6091c9ba6..bd004a77d86b 100644 --- a/packages/local_auth/local_auth/example/README.md +++ b/packages/local_auth/local_auth/example/README.md @@ -1,8 +1,3 @@ # local_auth_example Demonstrates how to use the local_auth plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). 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 d1cef4bf53a9..3c6eca7ce8a7 100644 --- a/packages/local_auth/local_auth/example/android/app/build.gradle +++ b/packages/local_auth/local_auth/example/android/app/build.gradle @@ -52,7 +52,7 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/packages/local_auth/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties index 186b71557c50..29e413457635 100644 --- a/packages/local_auth/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/local_auth/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/local_auth/local_auth/example/android/app/src/main/AndroidManifest.xml b/packages/local_auth/local_auth/example/android/app/src/main/AndroidManifest.xml index 8c091772107a..4acc4eb87ed6 100644 --- a/packages/local_auth/local_auth/example/android/app/src/main/AndroidManifest.xml +++ b/packages/local_auth/local_auth/example/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ package="io.flutter.plugins.localauthexample"> - + /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - B5AF6C7A6759E6F38749E537 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 3398D2C926163948005A052F /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 3398D2E426164AD8005A052F /* FLTLocalAuthPluginTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -371,14 +276,6 @@ }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 3398D2D326163948005A052F /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 3398D2D226163948005A052F /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -399,53 +296,6 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 3398D2D526163948005A052F /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 99302E79EC77497F2F274D12 /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Debug; - }; - 3398D2D626163948005A052F /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = FEA527BB0A821430FEAA1566 /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Release; - }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -597,15 +447,6 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 3398D2D426163948005A052F /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 3398D2D526163948005A052F /* Debug */, - 3398D2D626163948005A052F /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 58a5d07a15c8..b2af55dd6d37 100644 --- a/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { @@ -79,9 +81,11 @@ class _MyAppState extends State { _authorized = 'Authenticating'; }); authenticated = await auth.authenticate( - localizedReason: 'Let OS determine authentication method', - useErrorDialogs: true, - stickyAuth: true); + localizedReason: 'Let OS determine authentication method', + options: const AuthenticationOptions( + stickyAuth: true, + ), + ); setState(() { _isAuthenticating = false; }); @@ -109,11 +113,13 @@ class _MyAppState extends State { _authorized = 'Authenticating'; }); authenticated = await auth.authenticate( - localizedReason: - 'Scan your fingerprint (or face or whatever) to authenticate', - useErrorDialogs: true, + localizedReason: + 'Scan your fingerprint (or face or whatever) to authenticate', + options: const AuthenticationOptions( stickyAuth: true, - biometricOnly: true); + biometricOnly: true, + ), + ); setState(() { _isAuthenticating = false; _authorized = 'Authenticating'; @@ -163,14 +169,14 @@ class _MyAppState extends State { const Divider(height: 100), Text('Can check biometrics: $_canCheckBiometrics\n'), ElevatedButton( - child: const Text('Check biometrics'), onPressed: _checkBiometrics, + child: const Text('Check biometrics'), ), const Divider(height: 100), Text('Available biometrics: $_availableBiometrics\n'), ElevatedButton( - child: const Text('Get available biometrics'), onPressed: _getAvailableBiometrics, + child: const Text('Get available biometrics'), ), const Divider(height: 100), Text('Current State: $_authorized\n'), @@ -189,6 +195,7 @@ class _MyAppState extends State { Column( children: [ ElevatedButton( + onPressed: _authenticate, child: Row( mainAxisSize: MainAxisSize.min, children: const [ @@ -196,9 +203,9 @@ class _MyAppState extends State { Icon(Icons.perm_device_information), ], ), - onPressed: _authenticate, ), ElevatedButton( + onPressed: _authenticateWithBiometrics, child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -208,7 +215,6 @@ class _MyAppState extends State { const Icon(Icons.fingerprint), ], ), - onPressed: _authenticateWithBiometrics, ), ], ), diff --git a/packages/local_auth/local_auth/example/lib/readme_excerpts.dart b/packages/local_auth/local_auth/example/lib/readme_excerpts.dart new file mode 100644 index 000000000000..340aaef28f84 --- /dev/null +++ b/packages/local_auth/local_auth/example/lib/readme_excerpts.dart @@ -0,0 +1,166 @@ +// 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 exists solely to host compiled excerpts for README.md, and is not +// intended for use as an actual example application. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +// #docregion ErrorHandling +import 'package:flutter/services.dart'; +// #docregion NoErrorDialogs +import 'package:local_auth/error_codes.dart' as auth_error; +// #enddocregion NoErrorDialogs +// #docregion CanCheck +import 'package:local_auth/local_auth.dart'; +// #enddocregion CanCheck +// #enddocregion ErrorHandling + +// #docregion CustomMessages +import 'package:local_auth_android/local_auth_android.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; +// #enddocregion CustomMessages + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + // #docregion CanCheck + // #docregion ErrorHandling + final LocalAuthentication auth = LocalAuthentication(); + // #enddocregion CanCheck + // #enddocregion ErrorHandling + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('README example app'), + ), + body: const Text('See example in main.dart'), + ), + ); + } + + Future checkSupport() async { + // #docregion CanCheck + final bool canAuthenticateWithBiometrics = await auth.canCheckBiometrics; + final bool canAuthenticate = + canAuthenticateWithBiometrics || await auth.isDeviceSupported(); + // #enddocregion CanCheck + + print('Can authenticate: $canAuthenticate'); + print('Can authenticate with biometrics: $canAuthenticateWithBiometrics'); + } + + Future getEnrolledBiometrics() async { + // #docregion Enrolled + final List availableBiometrics = + await auth.getAvailableBiometrics(); + + if (availableBiometrics.isNotEmpty) { + // Some biometrics are enrolled. + } + + if (availableBiometrics.contains(BiometricType.strong) || + availableBiometrics.contains(BiometricType.face)) { + // Specific types of biometrics are available. + // Use checks like this with caution! + } + // #enddocregion Enrolled + } + + Future authenticate() async { + // #docregion AuthAny + try { + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance'); + // #enddocregion AuthAny + print(didAuthenticate); + // #docregion AuthAny + } on PlatformException { + // ... + } + // #enddocregion AuthAny + } + + Future authenticateWithBiometrics() async { + // #docregion AuthBioOnly + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance', + options: const AuthenticationOptions(biometricOnly: true)); + // #enddocregion AuthBioOnly + print(didAuthenticate); + } + + Future authenticateWithoutDialogs() async { + // #docregion NoErrorDialogs + try { + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance', + options: const AuthenticationOptions(useErrorDialogs: false)); + // #enddocregion NoErrorDialogs + print(didAuthenticate ? 'Success!' : 'Failure'); + // #docregion NoErrorDialogs + } on PlatformException catch (e) { + if (e.code == auth_error.notAvailable) { + // Add handling of no hardware here. + } else if (e.code == auth_error.notEnrolled) { + // ... + } else { + // ... + } + } + // #enddocregion NoErrorDialogs + } + + Future authenticateWithErrorHandling() async { + // #docregion ErrorHandling + try { + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance', + options: const AuthenticationOptions(useErrorDialogs: false)); + // #enddocregion ErrorHandling + print(didAuthenticate ? 'Success!' : 'Failure'); + // #docregion ErrorHandling + } on PlatformException catch (e) { + if (e.code == auth_error.notEnrolled) { + // Add handling of no hardware here. + } else if (e.code == auth_error.lockedOut || + e.code == auth_error.permanentlyLockedOut) { + // ... + } else { + // ... + } + } + // #enddocregion ErrorHandling + } + + Future authenticateWithCustomDialogMessages() async { + // #docregion CustomMessages + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance', + authMessages: const [ + AndroidAuthMessages( + signInTitle: 'Oops! Biometric authentication required!', + cancelButton: 'No thanks', + ), + IOSAuthMessages( + cancelButton: 'No thanks', + ), + ]); + // #enddocregion CustomMessages + print(didAuthenticate ? 'Success!' : 'Failure'); + } +} diff --git a/packages/local_auth/local_auth/example/pubspec.yaml b/packages/local_auth/local_auth/example/pubspec.yaml index ad0de2002a4c..f7dc2fc5b9e7 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.5.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -16,13 +16,17 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ + local_auth_android: ^1.0.0 + local_auth_ios: ^1.0.1 dev_dependencies: + build_runner: ^2.1.10 flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/local_auth/local_auth/example/windows/CMakeLists.txt b/packages/local_auth/local_auth/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..e013bd88bcb1 --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +set(BINARY_NAME "example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) \ No newline at end of file diff --git a/packages/local_auth/local_auth/example/windows/flutter/CMakeLists.txt b/packages/local_auth/local_auth/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..d83cc95319b6 --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) \ No newline at end of file diff --git a/packages/local_auth/local_auth/example/windows/flutter/generated_plugins.cmake b/packages/local_auth/local_auth/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..ef187dcae56f --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + local_auth_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/local_auth/local_auth/example/windows/runner/CMakeLists.txt b/packages/local_auth/local_auth/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..2520aa9e5fc7 --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) \ No newline at end of file diff --git a/packages/local_auth/local_auth/example/windows/runner/Runner.rc b/packages/local_auth/local_auth/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..7e35b9f56a22 --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "io.flutter.plugins" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 io.flutter.plugins. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/local_auth/local_auth/example/windows/runner/flutter_window.cpp b/packages/local_auth/local_auth/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..217bf9b69e67 --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/runner/flutter_window.cpp @@ -0,0 +1,64 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/local_auth/local_auth/example/windows/runner/flutter_window.h b/packages/local_auth/local_auth/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..7cbf3d3ebbb2 --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/runner/flutter_window.h @@ -0,0 +1,36 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/local_auth/local_auth/example/windows/runner/main.cpp b/packages/local_auth/local_auth/example/windows/runner/main.cpp new file mode 100644 index 000000000000..1285aabf714a --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/runner/main.cpp @@ -0,0 +1,45 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t* command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/local_auth/local_auth/example/windows/runner/resource.h b/packages/local_auth/local_auth/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/local_auth/local_auth/example/windows/runner/resources/app_icon.ico b/packages/local_auth/local_auth/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/local_auth/local_auth/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/local_auth/local_auth/example/windows/runner/runner.exe.manifest b/packages/local_auth/local_auth/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/local_auth/local_auth/example/windows/runner/utils.cpp b/packages/local_auth/local_auth/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..8b8eaa54539a --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/runner/utils.cpp @@ -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. +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE* unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = + ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, + nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/local_auth/local_auth/example/windows/runner/utils.h b/packages/local_auth/local_auth/example/windows/runner/utils.h new file mode 100644 index 000000000000..6d1cc48f0426 --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/runner/utils.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/local_auth/local_auth/example/windows/runner/win32_window.cpp b/packages/local_auth/local_auth/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..34738de2d35b --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/runner/win32_window.cpp @@ -0,0 +1,240 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { ++g_active_window_count; } + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { return window_handle_; } + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/packages/local_auth/local_auth/example/windows/runner/win32_window.h b/packages/local_auth/local_auth/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..0f8bd1b7f920 --- /dev/null +++ b/packages/local_auth/local_auth/example/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/local_auth/local_auth/lib/auth_strings.dart b/packages/local_auth/local_auth/lib/auth_strings.dart deleted file mode 100644 index 3e34659b8dad..000000000000 --- a/packages/local_auth/local_auth/lib/auth_strings.dart +++ /dev/null @@ -1,161 +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 is a temporary ignore to allow us to land a new set of linter rules in a -// series of manageable patches instead of one gigantic PR. It disables some of -// the new lints that are already failing on this plugin, for this plugin. It -// should be deleted and the failing lints addressed as soon as possible. -// ignore_for_file: public_member_api_docs - -import 'package:intl/intl.dart'; - -/// Android side authentication messages. -/// -/// Provides default values for all messages. -class AndroidAuthMessages { - const AndroidAuthMessages({ - this.biometricHint, - this.biometricNotRecognized, - this.biometricRequiredTitle, - this.biometricSuccess, - this.cancelButton, - this.deviceCredentialsRequiredTitle, - this.deviceCredentialsSetupDescription, - this.goToSettingsButton, - this.goToSettingsDescription, - this.signInTitle, - }); - - final String? biometricHint; - final String? biometricNotRecognized; - final String? biometricRequiredTitle; - final String? biometricSuccess; - final String? cancelButton; - final String? deviceCredentialsRequiredTitle; - final String? deviceCredentialsSetupDescription; - final String? goToSettingsButton; - final String? goToSettingsDescription; - final String? signInTitle; - - Map get args { - return { - 'biometricHint': biometricHint ?? androidBiometricHint, - 'biometricNotRecognized': - biometricNotRecognized ?? androidBiometricNotRecognized, - 'biometricSuccess': biometricSuccess ?? androidBiometricSuccess, - 'biometricRequired': - biometricRequiredTitle ?? androidBiometricRequiredTitle, - 'cancelButton': cancelButton ?? androidCancelButton, - 'deviceCredentialsRequired': deviceCredentialsRequiredTitle ?? - androidDeviceCredentialsRequiredTitle, - 'deviceCredentialsSetupDescription': deviceCredentialsSetupDescription ?? - androidDeviceCredentialsSetupDescription, - 'goToSetting': goToSettingsButton ?? goToSettings, - 'goToSettingDescription': - goToSettingsDescription ?? androidGoToSettingsDescription, - 'signInTitle': signInTitle ?? androidSignInTitle, - }; - } -} - -/// iOS side authentication messages. -/// -/// Provides default values for all messages. -class IOSAuthMessages { - const IOSAuthMessages({ - this.lockOut, - this.goToSettingsButton, - this.goToSettingsDescription, - this.cancelButton, - this.localizedFallbackTitle, - }); - - final String? lockOut; - final String? goToSettingsButton; - final String? goToSettingsDescription; - final String? cancelButton; - final String? localizedFallbackTitle; - - Map get args { - return { - 'lockOut': lockOut ?? iOSLockOut, - 'goToSetting': goToSettingsButton ?? goToSettings, - 'goToSettingDescriptionIOS': - goToSettingsDescription ?? iOSGoToSettingsDescription, - 'okButton': cancelButton ?? iOSOkButton, - if (localizedFallbackTitle != null) - 'localizedFallbackTitle': localizedFallbackTitle!, - }; - } -} - -// Strings for local_authentication plugin. Currently supports English. -// Intl.message must be string literals. -String get androidBiometricHint => Intl.message('Verify identity', - desc: - 'Hint message advising the user how to authenticate with biometrics. It is ' - 'used on Android side. Maximum 60 characters.'); - -String get androidBiometricNotRecognized => - Intl.message('Not recognized. Try again.', - desc: 'Message to let the user know that authentication was failed. It ' - 'is used on Android side. Maximum 60 characters.'); - -String get androidBiometricSuccess => Intl.message('Success', - desc: 'Message to let the user know that authentication was successful. It ' - 'is used on Android side. Maximum 60 characters.'); - -String get androidCancelButton => Intl.message('Cancel', - desc: 'Message showed on a button that the user can click to leave the ' - 'current dialog. It is used on Android side. Maximum 30 characters.'); - -String get androidSignInTitle => Intl.message('Authentication required', - desc: 'Message showed as a title in a dialog which indicates the user ' - 'that they need to scan biometric to continue. It is used on ' - 'Android side. Maximum 60 characters.'); - -String get androidBiometricRequiredTitle => Intl.message('Biometric required', - desc: 'Message showed as a title in a dialog which indicates the user ' - 'has not set up biometric authentication on their device. It is used on Android' - ' side. Maximum 60 characters.'); - -String get androidDeviceCredentialsRequiredTitle => Intl.message( - 'Device credentials required', - desc: 'Message showed as a title in a dialog which indicates the user ' - 'has not set up credentials authentication on their device. It is used on Android' - ' side. Maximum 60 characters.'); - -String get androidDeviceCredentialsSetupDescription => Intl.message( - 'Device credentials required', - desc: 'Message advising the user to go to the settings and configure ' - 'device credentials on their device. It shows in a dialog on Android side.'); - -String get goToSettings => Intl.message('Go to settings', - desc: 'Message showed on a button that the user can click to go to ' - 'settings pages from the current dialog. It is used on both Android ' - 'and iOS side. Maximum 30 characters.'); - -String get androidGoToSettingsDescription => Intl.message( - 'Biometric authentication is not set up on your device. Go to ' - '\'Settings > Security\' to add biometric authentication.', - desc: 'Message advising the user to go to the settings and configure ' - 'biometric on their device. It shows in a dialog on Android side.'); - -String get iOSLockOut => Intl.message( - 'Biometric authentication is disabled. Please lock and unlock your screen to ' - 'enable it.', - desc: - 'Message advising the user to re-enable biometrics on their device. It ' - 'shows in a dialog on iOS side.'); - -String get iOSGoToSettingsDescription => Intl.message( - 'Biometric authentication is not set up on your device. Please either enable ' - 'Touch ID or Face ID on your phone.', - desc: - 'Message advising the user to go to the settings and configure Biometrics ' - 'for their device. It shows in a dialog on iOS side.'); - -String get iOSOkButton => Intl.message('OK', - desc: 'Message showed on a button that the user can click to leave the ' - 'current dialog. It is used on iOS side. Maximum 30 characters.'); diff --git a/packages/local_auth/local_auth/lib/error_codes.dart b/packages/local_auth/local_auth/lib/error_codes.dart index bcf15b7b2154..8959bf297700 100644 --- a/packages/local_auth/local_auth/lib/error_codes.dart +++ b/packages/local_auth/local_auth/lib/error_codes.dart @@ -9,18 +9,21 @@ /// PIN/pattern/password (Android) on the device. const String passcodeNotSet = 'PasscodeNotSet'; -/// Indicates the user has not enrolled any fingerprints on the device. +/// Indicates the user has not enrolled any biometrics on the device. const String notEnrolled = 'NotEnrolled'; -/// Indicates the device does not have a Touch ID/fingerprint scanner. +/// Indicates the device does not have hardware support for biometrics. const String notAvailable = 'NotAvailable'; -/// Indicates the device operating system is not iOS or Android. +/// Indicates the device operating system is unsupported. const String otherOperatingSystem = 'OtherOperatingSystem'; -/// Indicates the API lock out due to too many attempts. +/// Indicates the API is temporarily locked out due to too many attempts. const String lockedOut = 'LockedOut'; -/// Indicates the API being disabled due to too many lock outs. +/// Indicates the API is locked out more persistently than [lockedOut]. /// Strong authentication like PIN/Pattern/Password is required to unlock. const String permanentlyLockedOut = 'PermanentlyLockedOut'; + +/// Indicates that the biometricOnly parameter can't be true on Windows +const String biometricOnlyNotSupported = 'biometricOnlyNotSupported'; diff --git a/packages/local_auth/local_auth/lib/local_auth.dart b/packages/local_auth/local_auth/lib/local_auth.dart index 3e925c00e5ae..7c42fedc7755 100644 --- a/packages/local_auth/local_auth/lib/local_auth.dart +++ b/packages/local_auth/local_auth/lib/local_auth.dart @@ -2,181 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// This is a temporary ignore to allow us to land a new set of linter rules in a -// series of manageable patches instead of one gigantic PR. It disables some of -// the new lints that are already failing on this plugin, for this plugin. It -// should be deleted and the failing lints addressed as soon as possible. -// ignore_for_file: public_member_api_docs - -import 'dart:async'; - -import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; -import 'package:platform/platform.dart'; - -import 'auth_strings.dart'; -import 'error_codes.dart'; - -enum BiometricType { face, fingerprint, iris } - -const MethodChannel _channel = MethodChannel('plugins.flutter.io/local_auth'); - -Platform _platform = const LocalPlatform(); - -@visibleForTesting -void setMockPathProviderPlatform(Platform platform) { - _platform = platform; -} - -/// A Flutter plugin for authenticating the user identity locally. -class LocalAuthentication { - /// The `authenticateWithBiometrics` method has been deprecated. - /// Use `authenticate` with `biometricOnly: true` instead - @Deprecated('Use `authenticate` with `biometricOnly: true` instead') - Future authenticateWithBiometrics({ - required String localizedReason, - bool useErrorDialogs = true, - bool stickyAuth = false, - AndroidAuthMessages androidAuthStrings = const AndroidAuthMessages(), - IOSAuthMessages iOSAuthStrings = const IOSAuthMessages(), - bool sensitiveTransaction = true, - }) => - authenticate( - localizedReason: localizedReason, - useErrorDialogs: useErrorDialogs, - stickyAuth: stickyAuth, - androidAuthStrings: androidAuthStrings, - iOSAuthStrings: iOSAuthStrings, - sensitiveTransaction: sensitiveTransaction, - biometricOnly: true, - ); - - /// Authenticates the user with biometrics available on the device while also - /// allowing the user to use device authentication - pin, pattern, passcode. - /// - /// Returns a [Future] holding true, if the user successfully authenticated, - /// false otherwise. - /// - /// [localizedReason] is the message to show to user while prompting them - /// for authentication. This is typically along the lines of: 'Please scan - /// your finger to access MyApp.'. This must not be empty. - /// - /// [useErrorDialogs] = true means the system will attempt to handle user - /// fixable issues encountered while authenticating. For instance, if - /// fingerprint reader exists on the phone but there's no fingerprint - /// registered, the plugin will attempt to take the user to settings to add - /// one. Anything that is not user fixable, such as no biometric sensor on - /// device, will be returned as a [PlatformException]. - /// - /// [stickyAuth] is used when the application goes into background for any - /// reason while the authentication is in progress. Due to security reasons, - /// the authentication has to be stopped at that time. If stickyAuth is set - /// to true, authentication resumes when the app is resumed. If it is set to - /// false (default), then as soon as app is paused a failure message is sent - /// back to Dart and it is up to the client app to restart authentication or - /// do something else. - /// - /// Construct [AndroidAuthStrings] and [IOSAuthStrings] if you want to - /// customize messages in the dialogs. - /// - /// Setting [sensitiveTransaction] to true enables platform specific - /// precautions. For instance, on face unlock, Android opens a confirmation - /// dialog after the face is recognized to make sure the user meant to unlock - /// their phone. - /// - /// Setting [biometricOnly] to true prevents authenticates from using non-biometric - /// local authentication such as pin, passcode, and passcode. - /// - /// Throws an [PlatformException] if there were technical problems with local - /// authentication (e.g. lack of relevant hardware). This might throw - /// [PlatformException] with error code [otherOperatingSystem] on the iOS - /// simulator. - Future authenticate({ - required String localizedReason, - bool useErrorDialogs = true, - bool stickyAuth = false, - AndroidAuthMessages androidAuthStrings = const AndroidAuthMessages(), - IOSAuthMessages iOSAuthStrings = const IOSAuthMessages(), - bool sensitiveTransaction = true, - bool biometricOnly = false, - }) async { - assert(localizedReason.isNotEmpty); - - final Map args = { - 'localizedReason': localizedReason, - 'useErrorDialogs': useErrorDialogs, - 'stickyAuth': stickyAuth, - 'sensitiveTransaction': sensitiveTransaction, - 'biometricOnly': biometricOnly, - }; - if (_platform.isIOS) { - args.addAll(iOSAuthStrings.args); - } else if (_platform.isAndroid) { - args.addAll(androidAuthStrings.args); - } else { - throw PlatformException( - code: otherOperatingSystem, - message: 'Local authentication does not support non-Android/iOS ' - 'operating systems.', - details: 'Your operating system is ${_platform.operatingSystem}', - ); - } - return (await _channel.invokeMethod('authenticate', args)) ?? false; - } - - /// Returns true if auth was cancelled successfully. - /// This api only works for Android. - /// Returns false if there was some error or no auth in progress. - /// - /// Returns [Future] bool true or false: - Future stopAuthentication() async { - if (_platform.isAndroid) { - return await _channel.invokeMethod('stopAuthentication') ?? false; - } - return true; - } - - /// Returns true if device is capable of checking biometrics - /// - /// Returns a [Future] bool true or false: - Future get canCheckBiometrics async => - (await _channel.invokeListMethod('getAvailableBiometrics'))! - .isNotEmpty; - - /// Returns true if device is capable of checking biometrics or is able to - /// fail over to device credentials. - /// - /// Returns a [Future] bool true or false: - Future isDeviceSupported() async => - (await _channel.invokeMethod('isDeviceSupported')) ?? false; - - /// Returns a list of enrolled biometrics - /// - /// Returns a [Future] List with the following possibilities: - /// - BiometricType.face - /// - BiometricType.fingerprint - /// - BiometricType.iris (not yet implemented) - Future> getAvailableBiometrics() async { - final List result = (await _channel.invokeListMethod( - 'getAvailableBiometrics', - )) ?? - []; - final List biometrics = []; - for (final String value in result) { - switch (value) { - case 'face': - biometrics.add(BiometricType.face); - break; - case 'fingerprint': - biometrics.add(BiometricType.fingerprint); - break; - case 'iris': - biometrics.add(BiometricType.iris); - break; - case 'undefined': - break; - } - } - return biometrics; - } -} +export 'package:local_auth/src/local_auth.dart' show LocalAuthentication; +export 'package:local_auth_platform_interface/types/auth_options.dart' + show AuthenticationOptions; +export 'package:local_auth_platform_interface/types/biometric_type.dart' + show BiometricType; diff --git a/packages/local_auth/local_auth/lib/src/local_auth.dart b/packages/local_auth/local_auth/lib/src/local_auth.dart new file mode 100644 index 000000000000..e369f67187a5 --- /dev/null +++ b/packages/local_auth/local_auth/lib/src/local_auth.dart @@ -0,0 +1,76 @@ +// 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 is a temporary ignore to allow us to land a new set of linter rules in a +// series of manageable patches instead of one gigantic PR. It disables some of +// the new lints that are already failing on this plugin, for this plugin. It +// should be deleted and the failing lints addressed as soon as possible. +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:local_auth_android/local_auth_android.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; +import 'package:local_auth_windows/local_auth_windows.dart'; + +/// A Flutter plugin for authenticating the user identity locally. +class LocalAuthentication { + /// Authenticates the user with biometrics available on the device while also + /// allowing the user to use device authentication - pin, pattern, passcode. + /// + /// Returns true if the user successfully authenticated, false otherwise. + /// + /// [localizedReason] is the message to show to user while prompting them + /// for authentication. This is typically along the lines of: 'Authenticate + /// to access MyApp.'. This must not be empty. + /// + /// Provide [authMessages] if you want to + /// customize messages in the dialogs. + /// + /// Provide [options] for configuring further authentication related options. + /// + /// Throws a [PlatformException] if there were technical problems with local + /// authentication (e.g. lack of relevant hardware). This might throw + /// [PlatformException] with error code [otherOperatingSystem] on the iOS + /// simulator. + Future authenticate( + {required String localizedReason, + Iterable authMessages = const [ + IOSAuthMessages(), + AndroidAuthMessages(), + WindowsAuthMessages() + ], + AuthenticationOptions options = const AuthenticationOptions()}) { + return LocalAuthPlatform.instance.authenticate( + localizedReason: localizedReason, + authMessages: authMessages, + options: options, + ); + } + + /// Cancels any in-progress authentication, returning true if auth was + /// cancelled successfully. + /// + /// This API is not supported by all platforms. + /// Returns false if there was some error, no authentication in progress, + /// or the current platform lacks support. + Future stopAuthentication() async { + return LocalAuthPlatform.instance.stopAuthentication(); + } + + /// Returns true if device is capable of checking biometrics. + Future get canCheckBiometrics => + LocalAuthPlatform.instance.deviceSupportsBiometrics(); + + /// Returns true if device is capable of checking biometrics or is able to + /// fail over to device credentials. + Future isDeviceSupported() async => + LocalAuthPlatform.instance.isDeviceSupported(); + + /// Returns a list of enrolled biometrics. + Future> getAvailableBiometrics() => + LocalAuthPlatform.instance.getEnrolledBiometrics(); +} diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index 78c79f4abce4..133df06d43b0 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -3,27 +3,30 @@ 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: 1.1.11 +version: 2.1.2 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: platforms: android: - package: io.flutter.plugins.localauth - pluginClass: LocalAuthPlugin + default_package: local_auth_android ios: - pluginClass: FLTLocalAuthPlugin + default_package: local_auth_ios + windows: + default_package: local_auth_windows dependencies: flutter: sdk: flutter - flutter_plugin_android_lifecycle: ^2.0.1 intl: ^0.17.0 - platform: ^3.0.0 + local_auth_android: ^1.0.0 + local_auth_ios: ^1.0.1 + local_auth_platform_interface: ^1.0.1 + local_auth_windows: ^1.0.0 dev_dependencies: flutter_driver: @@ -32,4 +35,5 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 + mockito: ^5.1.0 + plugin_platform_interface: ^2.1.2 diff --git a/packages/local_auth/local_auth/test/local_auth_test.dart b/packages/local_auth/local_auth/test/local_auth_test.dart index 3de9758f9d0c..00196a8b875e 100644 --- a/packages/local_auth/local_auth/test/local_auth_test.dart +++ b/packages/local_auth/local_auth/test/local_auth_test.dart @@ -2,255 +2,118 @@ // 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:local_auth/auth_strings.dart'; import 'package:local_auth/local_auth.dart'; -import 'package:platform/platform.dart'; +import 'package:local_auth_android/local_auth_android.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; +import 'package:local_auth_windows/local_auth_windows.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; void main() { - TestWidgetsFlutterBinding.ensureInitialized(); + WidgetsFlutterBinding.ensureInitialized(); + late LocalAuthentication localAuthentication; + late MockLocalAuthPlatform mockLocalAuthPlatform; - group('LocalAuth', () { - const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/local_auth', - ); + setUp(() { + localAuthentication = LocalAuthentication(); + mockLocalAuthPlatform = MockLocalAuthPlatform(); + LocalAuthPlatform.instance = mockLocalAuthPlatform; + }); - final List log = []; - late LocalAuthentication localAuthentication; + test('authenticate calls platform implementation', () { + when(mockLocalAuthPlatform.authenticate( + localizedReason: anyNamed('localizedReason'), + authMessages: anyNamed('authMessages'), + options: anyNamed('options'), + )).thenAnswer((_) async => true); + localAuthentication.authenticate(localizedReason: 'Test Reason'); + verify(mockLocalAuthPlatform.authenticate( + localizedReason: 'Test Reason', + authMessages: [ + const IOSAuthMessages(), + const AndroidAuthMessages(), + const WindowsAuthMessages(), + ], + )).called(1); + }); + + test('isDeviceSupported calls platform implementation', () { + when(mockLocalAuthPlatform.isDeviceSupported()) + .thenAnswer((_) async => true); + localAuthentication.isDeviceSupported(); + verify(mockLocalAuthPlatform.isDeviceSupported()).called(1); + }); - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall); - return Future.value(true); - }); - localAuthentication = LocalAuthentication(); - log.clear(); - }); + test('getEnrolledBiometrics calls platform implementation', () { + when(mockLocalAuthPlatform.getEnrolledBiometrics()) + .thenAnswer((_) async => []); + localAuthentication.getAvailableBiometrics(); + verify(mockLocalAuthPlatform.getEnrolledBiometrics()).called(1); + }); - group('With device auth fail over', () { - test('authenticate with no args on Android.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - biometricOnly: true, - ); - expect( - log, - [ - isMethodCall( - 'authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': true, - 'biometricHint': androidBiometricHint, - 'biometricNotRecognized': androidBiometricNotRecognized, - 'biometricSuccess': androidBiometricSuccess, - 'biometricRequired': androidBiometricRequiredTitle, - 'cancelButton': androidCancelButton, - 'deviceCredentialsRequired': - androidDeviceCredentialsRequiredTitle, - 'deviceCredentialsSetupDescription': - androidDeviceCredentialsSetupDescription, - 'goToSetting': goToSettings, - 'goToSettingDescription': androidGoToSettingsDescription, - 'signInTitle': androidSignInTitle, - }, - ), - ], - ); - }); + test('stopAuthentication calls platform implementation', () { + when(mockLocalAuthPlatform.stopAuthentication()) + .thenAnswer((_) async => true); + localAuthentication.stopAuthentication(); + verify(mockLocalAuthPlatform.stopAuthentication()).called(1); + }); - test('authenticate with no args on iOS.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - biometricOnly: true, - ); - expect( - log, - [ - isMethodCall('authenticate', arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': true, - 'lockOut': iOSLockOut, - 'goToSetting': goToSettings, - 'goToSettingDescriptionIOS': iOSGoToSettingsDescription, - 'okButton': iOSOkButton, - }), - ], - ); - }); + test('canCheckBiometrics returns correct result', () async { + when(mockLocalAuthPlatform.deviceSupportsBiometrics()) + .thenAnswer((_) async => false); + bool? result; + result = await localAuthentication.canCheckBiometrics; + expect(result, false); + when(mockLocalAuthPlatform.deviceSupportsBiometrics()) + .thenAnswer((_) async => true); + result = await localAuthentication.canCheckBiometrics; + expect(result, true); + verify(mockLocalAuthPlatform.deviceSupportsBiometrics()).called(2); + }); +} - test('authenticate with `localizedFallbackTitle` on iOS.', () async { - const IOSAuthMessages iosAuthMessages = - IOSAuthMessages(localizedFallbackTitle: 'Enter PIN'); - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - biometricOnly: true, - iOSAuthStrings: iosAuthMessages, - ); - expect( - log, - [ - isMethodCall('authenticate', arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': true, - 'lockOut': iOSLockOut, - 'goToSetting': goToSettings, - 'goToSettingDescriptionIOS': iOSGoToSettingsDescription, - 'okButton': iOSOkButton, - 'localizedFallbackTitle': 'Enter PIN', - }), - ], - ); - }); +class MockLocalAuthPlatform extends Mock + with MockPlatformInterfaceMixin + implements LocalAuthPlatform { + MockLocalAuthPlatform() { + throwOnMissingStub(this); + } - test('authenticate with no localizedReason on iOS.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - await expectLater( - localAuthentication.authenticate( - localizedReason: '', - biometricOnly: true, - ), - throwsAssertionError, - ); - }); + @override + Future authenticate({ + required String? localizedReason, + required Iterable? authMessages, + AuthenticationOptions? options = const AuthenticationOptions(), + }) => + super.noSuchMethod( + Invocation.method(#authenticate, [], { + #localizedReason: localizedReason, + #authMessages: authMessages, + #options: options, + }), + returnValue: Future.value(false)) as Future; - test('authenticate with no sensitive transaction.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticate( - localizedReason: 'Insecure', - sensitiveTransaction: false, - useErrorDialogs: false, - biometricOnly: true, - ); - expect( - log, - [ - isMethodCall('authenticate', arguments: { - 'localizedReason': 'Insecure', - 'useErrorDialogs': false, - 'stickyAuth': false, - 'sensitiveTransaction': false, - 'biometricOnly': true, - 'biometricHint': androidBiometricHint, - 'biometricNotRecognized': androidBiometricNotRecognized, - 'biometricSuccess': androidBiometricSuccess, - 'biometricRequired': androidBiometricRequiredTitle, - 'cancelButton': androidCancelButton, - 'deviceCredentialsRequired': - androidDeviceCredentialsRequiredTitle, - 'deviceCredentialsSetupDescription': - androidDeviceCredentialsSetupDescription, - 'goToSetting': goToSettings, - 'goToSettingDescription': androidGoToSettingsDescription, - 'signInTitle': androidSignInTitle, - }), - ], - ); - }); - }); + @override + Future> getEnrolledBiometrics() => + super.noSuchMethod(Invocation.method(#getEnrolledBiometrics, []), + returnValue: Future>.value([])) + as Future>; - group('With biometrics only', () { - test('authenticate with no args on Android.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - ); - expect( - log, - [ - isMethodCall('authenticate', arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - 'biometricHint': androidBiometricHint, - 'biometricNotRecognized': androidBiometricNotRecognized, - 'biometricSuccess': androidBiometricSuccess, - 'biometricRequired': androidBiometricRequiredTitle, - 'cancelButton': androidCancelButton, - 'deviceCredentialsRequired': - androidDeviceCredentialsRequiredTitle, - 'deviceCredentialsSetupDescription': - androidDeviceCredentialsSetupDescription, - 'goToSetting': goToSettings, - 'goToSettingDescription': androidGoToSettingsDescription, - 'signInTitle': androidSignInTitle, - }), - ], - ); - }); + @override + Future isDeviceSupported() => + super.noSuchMethod(Invocation.method(#isDeviceSupported, []), + returnValue: Future.value(false)) as Future; - test('authenticate with no args on iOS.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - ); - expect( - log, - [ - isMethodCall('authenticate', arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - 'lockOut': iOSLockOut, - 'goToSetting': goToSettings, - 'goToSettingDescriptionIOS': iOSGoToSettingsDescription, - 'okButton': iOSOkButton, - }), - ], - ); - }); + @override + Future stopAuthentication() => + super.noSuchMethod(Invocation.method(#stopAuthentication, []), + returnValue: Future.value(false)) as Future; - test('authenticate with no sensitive transaction.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticate( - localizedReason: 'Insecure', - sensitiveTransaction: false, - useErrorDialogs: false, - ); - expect( - log, - [ - isMethodCall('authenticate', arguments: { - 'localizedReason': 'Insecure', - 'useErrorDialogs': false, - 'stickyAuth': false, - 'sensitiveTransaction': false, - 'biometricOnly': false, - 'biometricHint': androidBiometricHint, - 'biometricNotRecognized': androidBiometricNotRecognized, - 'biometricSuccess': androidBiometricSuccess, - 'biometricRequired': androidBiometricRequiredTitle, - 'cancelButton': androidCancelButton, - 'deviceCredentialsRequired': - androidDeviceCredentialsRequiredTitle, - 'deviceCredentialsSetupDescription': - androidDeviceCredentialsSetupDescription, - 'goToSetting': goToSettings, - 'goToSettingDescription': androidGoToSettingsDescription, - 'signInTitle': androidSignInTitle, - }), - ], - ); - }); - }); - }); + @override + Future deviceSupportsBiometrics() => super.noSuchMethod( + Invocation.method(#deviceSupportsBiometrics, []), + returnValue: Future.value(false)) as Future; } diff --git a/packages/local_auth/local_auth_android/AUTHORS b/packages/local_auth/local_auth_android/AUTHORS new file mode 100644 index 000000000000..d5694690c247 --- /dev/null +++ b/packages/local_auth/local_auth_android/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Bodhi Mulders diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md new file mode 100644 index 000000000000..c9eeed9d01dd --- /dev/null +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -0,0 +1,66 @@ +## 1.0.14 + +* Fixes device credential authentication for API versions before R. + +## 1.0.13 + +* Updates imports for `prefer_relative_imports`. + +## 1.0.12 + +* Updates androidx.fragment version to 1.5.2. +* Updates minimum Flutter version to 2.10. + +## 1.0.11 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 1.0.10 + +* Updates `local_auth_platform_interface` constraint to the correct minimum + version. + +## 1.0.9 + +* Updates androidx.fragment version to 1.5.1. + +## 1.0.8 + +* Removes usages of `FingerprintManager` and other `BiometricManager` deprecated method usages. + +## 1.0.7 + +* Updates gradle version to 7.2.1. + +## 1.0.6 + +* Updates androidx.core version to 1.8.0. + +## 1.0.5 + +* Updates references to the obsolete master branch. + +## 1.0.4 + +* Minor fixes for new analysis options. + +## 1.0.3 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 1.0.2 + +* Fixes `getEnrolledBiometrics` to match documented behaviour: + Present biometrics that are not enrolled are no longer returned. +* `getEnrolledBiometrics` now only returns `weak` and `strong` biometric types. +* `deviceSupportsBiometrics` now returns the correct value regardless of enrollment state. + +## 1.0.1 + +* Adopts `Object.hash`. + +## 1.0.0 + +* Initial release from migration to federated architecture. diff --git a/packages/local_auth/local_auth_android/LICENSE b/packages/local_auth/local_auth_android/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/local_auth/local_auth_android/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/local_auth/local_auth_android/README.md b/packages/local_auth/local_auth_android/README.md new file mode 100644 index 000000000000..07244912f231 --- /dev/null +++ b/packages/local_auth/local_auth_android/README.md @@ -0,0 +1,11 @@ +# local\_auth\_android + +The Android implementation of [`local_auth`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `local_auth` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/local_auth +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin \ No newline at end of file diff --git a/packages/local_auth/local_auth/android/build.gradle b/packages/local_auth/local_auth_android/android/build.gradle similarity index 75% rename from packages/local_auth/local_auth/android/build.gradle rename to packages/local_auth/local_auth_android/android/build.gradle index de8ae50641b2..6c9417008d27 100644 --- a/packages/local_auth/local_auth/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:4.1.1' + classpath 'com.android.tools.build:gradle:7.2.1' } } @@ -29,6 +29,8 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { + // TODO(stuartmorgan): Enable when gradle is updated. + // disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' baseline file("lint-baseline.xml") @@ -49,11 +51,12 @@ android { } dependencies { - api "androidx.core:core:1.3.2" + api "androidx.core:core:1.8.0" api "androidx.biometric:biometric:1.1.0" - api "androidx.fragment:fragment:1.3.2" - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:3.9.0' + api "androidx.fragment:fragment:1.5.2" + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-inline:4.7.0' + testImplementation 'org.robolectric:robolectric:4.5' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/packages/local_auth/local_auth/android/lint-baseline.xml b/packages/local_auth/local_auth_android/android/lint-baseline.xml similarity index 100% rename from packages/local_auth/local_auth/android/lint-baseline.xml rename to packages/local_auth/local_auth_android/android/lint-baseline.xml diff --git a/packages/local_auth/local_auth/android/settings.gradle b/packages/local_auth/local_auth_android/android/settings.gradle similarity index 100% rename from packages/local_auth/local_auth/android/settings.gradle rename to packages/local_auth/local_auth_android/android/settings.gradle diff --git a/packages/local_auth/local_auth/android/src/main/AndroidManifest.xml b/packages/local_auth/local_auth_android/android/src/main/AndroidManifest.xml similarity index 69% rename from packages/local_auth/local_auth/android/src/main/AndroidManifest.xml rename to packages/local_auth/local_auth_android/android/src/main/AndroidManifest.xml index cb6cb985a986..63f75079e00d 100644 --- a/packages/local_auth/local_auth/android/src/main/AndroidManifest.xml +++ b/packages/local_auth/local_auth_android/android/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ - + diff --git a/packages/local_auth/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java similarity index 96% rename from packages/local_auth/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java rename to packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java index 2b825c6d1f31..c30f879d2c7f 100644 --- a/packages/local_auth/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java @@ -20,6 +20,7 @@ import android.view.View; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.biometric.BiometricManager; import androidx.biometric.BiometricPrompt; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.DefaultLifecycleObserver; @@ -90,11 +91,17 @@ interface AuthCompletionHandler { .setConfirmationRequired((Boolean) call.argument("sensitiveTransaction")) .setConfirmationRequired((Boolean) call.argument("sensitiveTransaction")); + int allowedAuthenticators = + BiometricManager.Authenticators.BIOMETRIC_WEAK + | BiometricManager.Authenticators.BIOMETRIC_STRONG; + if (allowCredentials) { - promptBuilder.setDeviceCredentialAllowed(true); + allowedAuthenticators |= BiometricManager.Authenticators.DEVICE_CREDENTIAL; } else { promptBuilder.setNegativeButtonText((String) call.argument("cancelButton")); } + + promptBuilder.setAllowedAuthenticators(allowedAuthenticators); this.promptInfo = promptBuilder.build(); } @@ -141,7 +148,6 @@ public void onAuthenticationError(int errorCode, CharSequence errString) { break; case BiometricPrompt.ERROR_NO_SPACE: case BiometricPrompt.ERROR_NO_BIOMETRICS: - if (promptInfo.isDeviceCredentialAllowed()) return; if (call.argument("useErrorDialogs")) { showGoToSettingsDialog( (String) call.argument("biometricRequired"), diff --git a/packages/local_auth/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java similarity index 68% rename from packages/local_auth/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java rename to packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java index a63e22a512d0..e545df01e7c0 100644 --- a/packages/local_auth/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java @@ -11,8 +11,6 @@ import android.app.KeyguardManager; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.hardware.fingerprint.FingerprintManager; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -40,17 +38,17 @@ */ @SuppressWarnings("deprecation") public class LocalAuthPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware { - private static final String CHANNEL_NAME = "plugins.flutter.io/local_auth"; + private static final String CHANNEL_NAME = "plugins.flutter.io/local_auth_android"; private static final int LOCK_REQUEST_CODE = 221; private Activity activity; - private final AtomicBoolean authInProgress = new AtomicBoolean(false); private AuthenticationHelper authHelper; + @VisibleForTesting final AtomicBoolean authInProgress = new AtomicBoolean(false); + // These are null when not using v2 embedding. private MethodChannel channel; private Lifecycle lifecycle; private BiometricManager biometricManager; - private FingerprintManager fingerprintManager; private KeyguardManager keyguardManager; private Result lockRequestResult; private final PluginRegistry.ActivityResultListener resultListener = @@ -101,8 +99,8 @@ public void onMethodCall(MethodCall call, @NonNull final Result result) { case "authenticate": authenticate(call, result); break; - case "getAvailableBiometrics": - getAvailableBiometrics(result); + case "getEnrolledBiometrics": + getEnrolledBiometrics(result); break; case "isDeviceSupported": isDeviceSupported(result); @@ -110,6 +108,9 @@ public void onMethodCall(MethodCall call, @NonNull final Result result) { case "stopAuthentication": stopAuthentication(result); break; + case "deviceSupportsBiometrics": + deviceSupportsBiometrics(result); + break; default: result.notImplemented(); break; @@ -145,79 +146,45 @@ private void authenticate(MethodCall call, final Result result) { } authInProgress.set(true); - AuthCompletionHandler completionHandler = - new AuthCompletionHandler() { - @Override - public void onSuccess() { - authenticateSuccess(result); - } + AuthCompletionHandler completionHandler = createAuthCompletionHandler(result); - @Override - public void onFailure() { - authenticateFail(result); - } + boolean isBiometricOnly = call.argument("biometricOnly"); + boolean allowCredentials = !isBiometricOnly && canAuthenticateWithDeviceCredential(); - @Override - public void onError(String code, String error) { - if (authInProgress.compareAndSet(true, false)) { - result.error(code, error, null); - } - } - }; + sendAuthenticationRequest(call, completionHandler, allowCredentials); + return; + } - // if is biometricOnly try biometric prompt - might not work - boolean isBiometricOnly = call.argument("biometricOnly"); - if (isBiometricOnly) { - if (!canAuthenticateWithBiometrics()) { - if (!hasBiometricHardware()) { - completionHandler.onError("NoHardware", "No biometric hardware found"); - } - completionHandler.onError("NotEnrolled", "No biometrics enrolled on this device."); - return; + @VisibleForTesting + public AuthCompletionHandler createAuthCompletionHandler(final Result result) { + return new AuthCompletionHandler() { + @Override + public void onSuccess() { + authenticateSuccess(result); } - authHelper = - new AuthenticationHelper( - lifecycle, (FragmentActivity) activity, call, completionHandler, false); - authHelper.authenticate(); - return; - } - // API 29 and above - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - authHelper = - new AuthenticationHelper( - lifecycle, (FragmentActivity) activity, call, completionHandler, true); - authHelper.authenticate(); - return; - } + @Override + public void onFailure() { + authenticateFail(result); + } - // API 23 - 28 with fingerprint - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && fingerprintManager != null) { - if (fingerprintManager.hasEnrolledFingerprints()) { - authHelper = - new AuthenticationHelper( - lifecycle, (FragmentActivity) activity, call, completionHandler, false); - authHelper.authenticate(); - return; + @Override + public void onError(String code, String error) { + if (authInProgress.compareAndSet(true, false)) { + result.error(code, error, null); + } } - } + }; + } - // API 23 or higher with device credentials - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - && keyguardManager != null - && keyguardManager.isDeviceSecure()) { - String title = call.argument("signInTitle"); - String reason = call.argument("localizedReason"); - Intent authIntent = keyguardManager.createConfirmDeviceCredentialIntent(title, reason); - - // save result for async response - lockRequestResult = result; - activity.startActivityForResult(authIntent, LOCK_REQUEST_CODE); - return; - } + @VisibleForTesting + public void sendAuthenticationRequest( + MethodCall call, AuthCompletionHandler completionHandler, boolean allowCredentials) { + authHelper = + new AuthenticationHelper( + lifecycle, (FragmentActivity) activity, call, completionHandler, allowCredentials); - // Unable to authenticate - result.error("NotSupported", "This device does not support required security features", null); + authHelper.authenticate(); } private void authenticateSuccess(Result result) { @@ -248,58 +215,78 @@ private void stopAuthentication(Result result) { } } + private void deviceSupportsBiometrics(final Result result) { + result.success(hasBiometricHardware()); + } + /* - * Returns biometric types available on device + * Returns enrolled biometric types available on device. */ - private void getAvailableBiometrics(final Result result) { + private void getEnrolledBiometrics(final Result result) { try { if (activity == null || activity.isFinishing()) { result.error("no_activity", "local_auth plugin requires a foreground activity", null); return; } - ArrayList biometrics = getAvailableBiometrics(); + ArrayList biometrics = getEnrolledBiometrics(); result.success(biometrics); } catch (Exception e) { result.error("no_biometrics_available", e.getMessage(), null); } } - private ArrayList getAvailableBiometrics() { + @VisibleForTesting + public ArrayList getEnrolledBiometrics() { ArrayList biometrics = new ArrayList<>(); if (activity == null || activity.isFinishing()) { return biometrics; } - PackageManager packageManager = activity.getPackageManager(); - if (Build.VERSION.SDK_INT >= 23) { - if (packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { - biometrics.add("fingerprint"); - } + if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) + == BiometricManager.BIOMETRIC_SUCCESS) { + biometrics.add("weak"); } - if (Build.VERSION.SDK_INT >= 29) { - if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) { - biometrics.add("face"); - } - if (packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)) { - biometrics.add("iris"); - } + if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) + == BiometricManager.BIOMETRIC_SUCCESS) { + biometrics.add("strong"); } - return biometrics; } - private boolean isDeviceSupported() { + @VisibleForTesting + public boolean isDeviceSecure() { if (keyguardManager == null) return false; return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && keyguardManager.isDeviceSecure()); } + @VisibleForTesting + public boolean isDeviceSupported() { + return isDeviceSecure() || canAuthenticateWithBiometrics(); + } + private boolean canAuthenticateWithBiometrics() { if (biometricManager == null) return false; - return biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS; + return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) + == BiometricManager.BIOMETRIC_SUCCESS; } private boolean hasBiometricHardware() { if (biometricManager == null) return false; - return biometricManager.canAuthenticate() != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE; + return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) + != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE; + } + + @VisibleForTesting + public boolean canAuthenticateWithDeviceCredential() { + if (Build.VERSION.SDK_INT < 30) { + // Checking for device credential only authentication via the BiometricManager + // is not allowed before API level 30, so we check for presence of PIN, pattern, + // or password instead. + return isDeviceSecure(); + } + + if (biometricManager == null) return false; + return biometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + == BiometricManager.BIOMETRIC_SUCCESS; } private void isDeviceSupported(Result result) { @@ -321,10 +308,6 @@ private void setServicesFromActivity(Activity activity) { Context context = activity.getBaseContext(); biometricManager = BiometricManager.from(activity); keyguardManager = (KeyguardManager) context.getSystemService(KEYGUARD_SERVICE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - fingerprintManager = - (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE); - } } @Override @@ -359,4 +342,14 @@ public void onDetachedFromActivity() { final Activity getActivity() { return activity; } + + @VisibleForTesting + void setBiometricManager(BiometricManager biometricManager) { + this.biometricManager = biometricManager; + } + + @VisibleForTesting + void setKeyguardManager(KeyguardManager keyguardManager) { + this.keyguardManager = keyguardManager; + } } diff --git a/packages/local_auth/local_auth/android/src/main/res/drawable/fingerprint_initial_icon.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_initial_icon.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/drawable/fingerprint_initial_icon.xml rename to packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_initial_icon.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/drawable/fingerprint_success_icon.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_success_icon.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/drawable/fingerprint_success_icon.xml rename to packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_success_icon.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/drawable/fingerprint_warning_icon.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_warning_icon.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/drawable/fingerprint_warning_icon.xml rename to packages/local_auth/local_auth_android/android/src/main/res/drawable/fingerprint_warning_icon.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/drawable/ic_done_white_24dp.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_done_white_24dp.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/drawable/ic_done_white_24dp.xml rename to packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_done_white_24dp.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/drawable/ic_fingerprint_white_24dp.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_fingerprint_white_24dp.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/drawable/ic_fingerprint_white_24dp.xml rename to packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_fingerprint_white_24dp.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/drawable/ic_priority_high_white_24dp.xml b/packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_priority_high_white_24dp.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/drawable/ic_priority_high_white_24dp.xml rename to packages/local_auth/local_auth_android/android/src/main/res/drawable/ic_priority_high_white_24dp.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/layout/go_to_setting.xml b/packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/layout/go_to_setting.xml rename to packages/local_auth/local_auth_android/android/src/main/res/layout/go_to_setting.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/layout/scan_fp.xml b/packages/local_auth/local_auth_android/android/src/main/res/layout/scan_fp.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/layout/scan_fp.xml rename to packages/local_auth/local_auth_android/android/src/main/res/layout/scan_fp.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/values/colors.xml b/packages/local_auth/local_auth_android/android/src/main/res/values/colors.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/values/colors.xml rename to packages/local_auth/local_auth_android/android/src/main/res/values/colors.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/values/dimens.xml b/packages/local_auth/local_auth_android/android/src/main/res/values/dimens.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/values/dimens.xml rename to packages/local_auth/local_auth_android/android/src/main/res/values/dimens.xml diff --git a/packages/local_auth/local_auth/android/src/main/res/values/styles.xml b/packages/local_auth/local_auth_android/android/src/main/res/values/styles.xml similarity index 100% rename from packages/local_auth/local_auth/android/src/main/res/values/styles.xml rename to packages/local_auth/local_auth_android/android/src/main/res/values/styles.xml diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java new file mode 100644 index 000000000000..7279a3c49af2 --- /dev/null +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java @@ -0,0 +1,409 @@ +// 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.localauth; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.app.KeyguardManager; +import android.app.NativeActivity; +import android.content.Context; +import androidx.biometric.BiometricManager; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.Lifecycle; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +public class LocalAuthTest { + @Test + public void authenticate_returnsErrorWhenAuthInProgress() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + plugin.authInProgress.set(true); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + plugin.onMethodCall(new MethodCall("authenticate", null), mockResult); + verify(mockResult).error("auth_in_progress", "Authentication in progress", null); + } + + @Test + public void authenticate_returnsErrorWithNoForegroundActivity() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + plugin.onMethodCall(new MethodCall("authenticate", null), mockResult); + verify(mockResult) + .error("no_activity", "local_auth plugin requires a foreground activity", null); + } + + @Test + public void authenticate_returnsErrorWhenActivityNotFragmentActivity() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + setPluginActivity(plugin, buildMockActivityWithContext(mock(NativeActivity.class))); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + plugin.onMethodCall(new MethodCall("authenticate", null), mockResult); + verify(mockResult) + .error( + "no_fragment_activity", + "local_auth plugin requires activity to be a FragmentActivity.", + null); + } + + @Test + public void authenticate_returnsErrorWhenDeviceNotSupported() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + setPluginActivity(plugin, buildMockActivityWithContext(mock(FragmentActivity.class))); + plugin.onMethodCall(new MethodCall("authenticate", null), mockResult); + assertFalse(plugin.authInProgress.get()); + verify(mockResult).error("NotAvailable", "Required security features not enabled", null); + } + + @Test + public void authenticate_properlyConfiguresBiometricOnlyAuthenticationRequest() { + final LocalAuthPlugin plugin = spy(new LocalAuthPlugin()); + setPluginActivity(plugin, buildMockActivityWithContext(mock(FragmentActivity.class))); + when(plugin.isDeviceSupported()).thenReturn(true); + + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + plugin.setBiometricManager(mockBiometricManager); + + ArgumentCaptor allowCredentialsCaptor = ArgumentCaptor.forClass(Boolean.class); + doNothing() + .when(plugin) + .sendAuthenticationRequest( + any(MethodCall.class), + any(AuthCompletionHandler.class), + allowCredentialsCaptor.capture()); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + HashMap arguments = new HashMap<>(); + arguments.put("biometricOnly", true); + + plugin.onMethodCall(new MethodCall("authenticate", arguments), mockResult); + assertFalse(allowCredentialsCaptor.getValue()); + } + + @Test + @Config(sdk = 30) + public void authenticate_properlyConfiguresBiometricAndDeviceCredentialAuthenticationRequest() { + final LocalAuthPlugin plugin = spy(new LocalAuthPlugin()); + setPluginActivity(plugin, buildMockActivityWithContext(mock(FragmentActivity.class))); + when(plugin.isDeviceSupported()).thenReturn(true); + + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + plugin.setBiometricManager(mockBiometricManager); + + ArgumentCaptor allowCredentialsCaptor = ArgumentCaptor.forClass(Boolean.class); + doNothing() + .when(plugin) + .sendAuthenticationRequest( + any(MethodCall.class), + any(AuthCompletionHandler.class), + allowCredentialsCaptor.capture()); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + HashMap arguments = new HashMap<>(); + arguments.put("biometricOnly", false); + + plugin.onMethodCall(new MethodCall("authenticate", arguments), mockResult); + assertTrue(allowCredentialsCaptor.getValue()); + } + + @Test + @Config(sdk = 30) + public void authenticate_properlyConfiguresDeviceCredentialOnlyAuthenticationRequest() { + final LocalAuthPlugin plugin = spy(new LocalAuthPlugin()); + setPluginActivity(plugin, buildMockActivityWithContext(mock(FragmentActivity.class))); + when(plugin.isDeviceSupported()).thenReturn(true); + + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + plugin.setBiometricManager(mockBiometricManager); + + ArgumentCaptor allowCredentialsCaptor = ArgumentCaptor.forClass(Boolean.class); + doNothing() + .when(plugin) + .sendAuthenticationRequest( + any(MethodCall.class), + any(AuthCompletionHandler.class), + allowCredentialsCaptor.capture()); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + HashMap arguments = new HashMap<>(); + arguments.put("biometricOnly", false); + + plugin.onMethodCall(new MethodCall("authenticate", arguments), mockResult); + assertTrue(allowCredentialsCaptor.getValue()); + } + + @Test + public void isDeviceSupportedReturnsFalse() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + plugin.onMethodCall(new MethodCall("isDeviceSupported", null), mockResult); + verify(mockResult).success(false); + } + + @Test + public void deviceSupportsBiometrics_returnsTrueForPresentNonEnrolledBiometrics() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED); + plugin.setBiometricManager(mockBiometricManager); + plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult); + verify(mockResult).success(true); + } + + @Test + public void deviceSupportsBiometrics_returnsTrueForPresentEnrolledBiometrics() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + plugin.setBiometricManager(mockBiometricManager); + plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult); + verify(mockResult).success(true); + } + + @Test + public void deviceSupportsBiometrics_returnsFalseForNoBiometricHardware() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE); + plugin.setBiometricManager(mockBiometricManager); + plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult); + verify(mockResult).success(false); + } + + @Test + public void deviceSupportsBiometrics_returnsFalseForNullBiometricManager() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + plugin.setBiometricManager(null); + plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult); + verify(mockResult).success(false); + } + + @Test + public void onDetachedFromActivity_ShouldReleaseActivity() { + final Activity mockActivity = mock(Activity.class); + final ActivityPluginBinding mockActivityBinding = mock(ActivityPluginBinding.class); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + + Context mockContext = mock(Context.class); + when(mockActivity.getBaseContext()).thenReturn(mockContext); + when(mockActivity.getApplicationContext()).thenReturn(mockContext); + + final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class); + when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference); + + final Lifecycle mockLifecycle = mock(Lifecycle.class); + when(mockLifecycleReference.getLifecycle()).thenReturn(mockLifecycle); + + final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); + final FlutterEngine mockFlutterEngine = mock(FlutterEngine.class); + when(mockPluginBinding.getFlutterEngine()).thenReturn(mockFlutterEngine); + + DartExecutor mockDartExecutor = mock(DartExecutor.class); + when(mockFlutterEngine.getDartExecutor()).thenReturn(mockDartExecutor); + + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + plugin.onAttachedToEngine(mockPluginBinding); + plugin.onAttachedToActivity(mockActivityBinding); + assertNotNull(plugin.getActivity()); + + plugin.onDetachedFromActivity(); + assertNull(plugin.getActivity()); + } + + @Test + public void getEnrolledBiometrics_shouldReturnError_whenNoActivity() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult); + verify(mockResult) + .error("no_activity", "local_auth plugin requires a foreground activity", null); + } + + @Test + public void getEnrolledBiometrics_shouldReturnError_whenFinishingActivity() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final Activity mockActivity = buildMockActivityWithContext(mock(Activity.class)); + when(mockActivity.isFinishing()).thenReturn(true); + setPluginActivity(plugin, mockActivity); + + plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult); + verify(mockResult) + .error("no_activity", "local_auth plugin requires a foreground activity", null); + } + + @Test + public void getEnrolledBiometrics_shouldReturnEmptyList_withoutHardwarePresent() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + setPluginActivity(plugin, buildMockActivityWithContext(mock(Activity.class))); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(anyInt())) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE); + plugin.setBiometricManager(mockBiometricManager); + + plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult); + verify(mockResult).success(Collections.emptyList()); + } + + @Test + public void getEnrolledBiometrics_shouldReturnEmptyList_withNoMethodsEnrolled() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + setPluginActivity(plugin, buildMockActivityWithContext(mock(Activity.class))); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(anyInt())) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED); + plugin.setBiometricManager(mockBiometricManager); + + plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult); + verify(mockResult).success(Collections.emptyList()); + } + + @Test + public void getEnrolledBiometrics_shouldOnlyAddEnrolledBiometrics() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + setPluginActivity(plugin, buildMockActivityWithContext(mock(Activity.class))); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED); + plugin.setBiometricManager(mockBiometricManager); + + plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult); + verify(mockResult) + .success( + new ArrayList() { + { + add("weak"); + } + }); + } + + @Test + public void getEnrolledBiometrics_shouldAddStrongBiometrics() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + setPluginActivity(plugin, buildMockActivityWithContext(mock(Activity.class))); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + plugin.setBiometricManager(mockBiometricManager); + + plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult); + verify(mockResult) + .success( + new ArrayList() { + { + add("weak"); + add("strong"); + } + }); + } + + @Test + @Config(sdk = 22) + public void isDeviceSecure_returnsFalseOnBelowApi23() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + assertFalse(plugin.isDeviceSecure()); + } + + @Test + @Config(sdk = 23) + public void isDeviceSecure_returnsTrueIfDeviceIsSecure() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + KeyguardManager mockKeyguardManager = mock(KeyguardManager.class); + plugin.setKeyguardManager(mockKeyguardManager); + + when(mockKeyguardManager.isDeviceSecure()).thenReturn(true); + assertTrue(plugin.isDeviceSecure()); + + when(mockKeyguardManager.isDeviceSecure()).thenReturn(false); + assertFalse(plugin.isDeviceSecure()); + } + + @Test + @Config(sdk = 30) + public void + canAuthenticateWithDeviceCredential_returnsTrueIfHasBiometricManagerSupportAboveApi30() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + plugin.setBiometricManager(mockBiometricManager); + + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + assertTrue(plugin.canAuthenticateWithDeviceCredential()); + + when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL)) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED); + assertFalse(plugin.canAuthenticateWithDeviceCredential()); + } + + private Activity buildMockActivityWithContext(Activity mockActivity) { + final Context mockContext = mock(Context.class); + when(mockActivity.getBaseContext()).thenReturn(mockContext); + when(mockActivity.getApplicationContext()).thenReturn(mockContext); + return mockActivity; + } + + private void setPluginActivity(LocalAuthPlugin plugin, Activity activity) { + final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class); + final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); + final ActivityPluginBinding mockActivityBinding = mock(ActivityPluginBinding.class); + final FlutterEngine mockFlutterEngine = mock(FlutterEngine.class); + final DartExecutor mockDartExecutor = mock(DartExecutor.class); + when(mockPluginBinding.getFlutterEngine()).thenReturn(mockFlutterEngine); + when(mockFlutterEngine.getDartExecutor()).thenReturn(mockDartExecutor); + when(mockActivityBinding.getActivity()).thenReturn(activity); + when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference); + plugin.onAttachedToEngine(mockPluginBinding); + plugin.onAttachedToActivity(mockActivityBinding); + } +} diff --git a/packages/local_auth/local_auth_android/example/README.md b/packages/local_auth/local_auth_android/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/local_auth/local_auth_android/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/local_auth/local_auth_android/example/android/app/build.gradle b/packages/local_auth/local_auth_android/example/android/app/build.gradle new file mode 100644 index 000000000000..3c6eca7ce8a7 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/app/build.gradle @@ -0,0 +1,58 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.localauthexample" + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/packages/local_auth/local_auth_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth_android/example/android/app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..29e413457635 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -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. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java b/packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java new file mode 100644 index 000000000000..68c22371d7dd --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java @@ -0,0 +1,20 @@ +// 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.localauth; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterFragmentActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterFragmentActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(FlutterFragmentActivity.class); +} diff --git a/packages/local_auth/local_auth_android/example/android/app/src/main/AndroidManifest.xml b/packages/local_auth/local_auth_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..4acc4eb87ed6 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/local_auth/local_auth_android/example/android/build.gradle b/packages/local_auth/local_auth_android/example/android/build.gradle new file mode 100644 index 000000000000..c21bff8e0a2f --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.0.1' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/local_auth/local_auth_android/example/android/gradle.properties b/packages/local_auth/local_auth_android/example/android/gradle.properties new file mode 100644 index 000000000000..7fe61a74cee0 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1024m +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 new file mode 100644 index 000000000000..3f383641d7c3 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Jan 03 14:07:08 CST 2021 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/local_auth/local_auth_android/example/android/settings.gradle b/packages/local_auth/local_auth_android/example/android/settings.gradle new file mode 100644 index 000000000000..115da6cb4f4d --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withInputStream { stream -> plugins.load(stream) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/packages/local_auth/local_auth_android/example/android/settings_aar.gradle b/packages/local_auth/local_auth_android/example/android/settings_aar.gradle new file mode 100644 index 000000000000..e7b4def49cb5 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/packages/local_auth/local_auth_android/example/integration_test/local_auth_test.dart b/packages/local_auth/local_auth_android/example/integration_test/local_auth_test.dart new file mode 100644 index 000000000000..1dfc0ae7a6d6 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/integration_test/local_auth_test.dart @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:local_auth_android/local_auth_android.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('canCheckBiometrics', (WidgetTester tester) async { + expect( + LocalAuthAndroid().getEnrolledBiometrics(), + completion(isList), + ); + }); +} diff --git a/packages/local_auth/local_auth_android/example/lib/main.dart b/packages/local_auth/local_auth_android/example/lib/main.dart new file mode 100644 index 000000000000..9909853a62af --- /dev/null +++ b/packages/local_auth/local_auth_android/example/lib/main.dart @@ -0,0 +1,239 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:local_auth_android/local_auth_android.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + _SupportState _supportState = _SupportState.unknown; + bool? _deviceSupportsBiometrics; + List? _enrolledBiometrics; + String _authorized = 'Not Authorized'; + bool _isAuthenticating = false; + + @override + void initState() { + super.initState(); + LocalAuthPlatform.instance.isDeviceSupported().then( + (bool isSupported) => setState(() => _supportState = isSupported + ? _SupportState.supported + : _SupportState.unsupported), + ); + } + + Future _checkBiometrics() async { + late bool deviceSupportsBiometrics; + try { + deviceSupportsBiometrics = + await LocalAuthPlatform.instance.deviceSupportsBiometrics(); + } on PlatformException catch (e) { + deviceSupportsBiometrics = false; + print(e); + } + if (!mounted) { + return; + } + + setState(() { + _deviceSupportsBiometrics = deviceSupportsBiometrics; + }); + } + + Future _getEnrolledBiometrics() async { + late List availableBiometrics; + try { + availableBiometrics = + await LocalAuthPlatform.instance.getEnrolledBiometrics(); + } on PlatformException catch (e) { + availableBiometrics = []; + print(e); + } + if (!mounted) { + return; + } + + setState(() { + _enrolledBiometrics = availableBiometrics; + }); + } + + Future _authenticate() async { + bool authenticated = false; + try { + setState(() { + _isAuthenticating = true; + _authorized = 'Authenticating'; + }); + authenticated = await LocalAuthPlatform.instance.authenticate( + localizedReason: 'Let OS determine authentication method', + authMessages: [const AndroidAuthMessages()], + options: const AuthenticationOptions( + stickyAuth: true, + ), + ); + setState(() { + _isAuthenticating = false; + }); + } on PlatformException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = 'Error - ${e.message}'; + }); + return; + } + if (!mounted) { + return; + } + + setState( + () => _authorized = authenticated ? 'Authorized' : 'Not Authorized'); + } + + Future _authenticateWithBiometrics() async { + bool authenticated = false; + try { + setState(() { + _isAuthenticating = true; + _authorized = 'Authenticating'; + }); + authenticated = await LocalAuthPlatform.instance.authenticate( + localizedReason: + 'Scan your fingerprint (or face or whatever) to authenticate', + authMessages: [const AndroidAuthMessages()], + 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); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: ListView( + padding: const EdgeInsets.only(top: 30), + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_supportState == _SupportState.unknown) + const CircularProgressIndicator() + else if (_supportState == _SupportState.supported) + const Text('This device is supported') + else + const Text('This device is not supported'), + const Divider(height: 100), + Text( + 'Device supports biometrics: $_deviceSupportsBiometrics\n'), + ElevatedButton( + onPressed: _checkBiometrics, + child: const Text('Check biometrics'), + ), + const Divider(height: 100), + Text('Enrolled biometrics: $_enrolledBiometrics\n'), + ElevatedButton( + onPressed: _getEnrolledBiometrics, + child: const Text('Get enrolled biometrics'), + ), + const Divider(height: 100), + Text('Current State: $_authorized\n'), + if (_isAuthenticating) + ElevatedButton( + onPressed: _cancelAuthentication, + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Text('Cancel Authentication'), + Icon(Icons.cancel), + ], + ), + ) + else + Column( + children: [ + ElevatedButton( + onPressed: _authenticate, + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Text('Authenticate'), + Icon(Icons.perm_device_information), + ], + ), + ), + ElevatedButton( + onPressed: _authenticateWithBiometrics, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_isAuthenticating + ? 'Cancel' + : 'Authenticate: biometrics only'), + const Icon(Icons.fingerprint), + ], + ), + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} + +enum _SupportState { + unknown, + supported, + unsupported, +} diff --git a/packages/local_auth/local_auth_android/example/pubspec.yaml b/packages/local_auth/local_auth_android/example/pubspec.yaml new file mode 100644 index 000000000000..c95b89ad0c2a --- /dev/null +++ b/packages/local_auth/local_auth_android/example/pubspec.yaml @@ -0,0 +1,30 @@ +name: local_auth_android_example +description: Demonstrates how to use the local_auth_android plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + flutter: + sdk: flutter + local_auth_android: + # When depending on this package from a real application you should use: + # local_auth_android: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + local_auth_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/local_auth/local_auth_android/example/test_driver/integration_test.dart b/packages/local_auth/local_auth_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/local_auth/local_auth_android/example/test_driver/integration_test.dart @@ -0,0 +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. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/local_auth/local_auth_android/lib/local_auth_android.dart b/packages/local_auth/local_auth_android/lib/local_auth_android.dart new file mode 100644 index 000000000000..e2134173691e --- /dev/null +++ b/packages/local_auth/local_auth_android/lib/local_auth_android.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/services.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; + +import 'types/auth_messages_android.dart'; + +export 'package:local_auth_android/types/auth_messages_android.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'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/local_auth_android'); + +/// The implementation of [LocalAuthPlatform] for Android. +class LocalAuthAndroid extends LocalAuthPlatform { + /// Registers this class as the default instance of [LocalAuthPlatform]. + static void registerWith() { + LocalAuthPlatform.instance = LocalAuthAndroid(); + } + + @override + Future authenticate({ + required String localizedReason, + required Iterable authMessages, + 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 AndroidAuthMessages().args); + for (final AuthMessages messages in authMessages) { + if (messages is AndroidAuthMessages) { + args.addAll(messages.args); + } + } + return (await _channel.invokeMethod('authenticate', args)) ?? false; + } + + @override + Future deviceSupportsBiometrics() async { + return (await _channel.invokeMethod('deviceSupportsBiometrics')) ?? + false; + } + + @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; + } + } + return biometrics; + } + + @override + Future isDeviceSupported() async => + (await _channel.invokeMethod('isDeviceSupported')) ?? false; + + @override + Future stopAuthentication() async => + await _channel.invokeMethod('stopAuthentication') ?? false; +} diff --git a/packages/local_auth/local_auth_android/lib/types/auth_messages_android.dart b/packages/local_auth/local_auth_android/lib/types/auth_messages_android.dart new file mode 100644 index 000000000000..c82f6820055c --- /dev/null +++ b/packages/local_auth/local_auth_android/lib/types/auth_messages_android.dart @@ -0,0 +1,192 @@ +// 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:intl/intl.dart'; +import 'package:local_auth_platform_interface/types/auth_messages.dart'; + +/// Android side authentication messages. +/// +/// Provides default values for all messages. +@immutable +class AndroidAuthMessages extends AuthMessages { + /// Constructs a new instance. + const AndroidAuthMessages({ + this.biometricHint, + this.biometricNotRecognized, + this.biometricRequiredTitle, + this.biometricSuccess, + this.cancelButton, + this.deviceCredentialsRequiredTitle, + this.deviceCredentialsSetupDescription, + this.goToSettingsButton, + this.goToSettingsDescription, + this.signInTitle, + }); + + /// Hint message advising the user how to authenticate with biometrics. + /// Maximum 60 characters. + final String? biometricHint; + + /// Message to let the user know that authentication was failed. + /// Maximum 60 characters. + final String? biometricNotRecognized; + + /// Message shown as a title in a dialog which indicates the user + /// has not set up biometric authentication on their device. + /// Maximum 60 characters. + final String? biometricRequiredTitle; + + /// Message to let the user know that authentication was successful. + /// Maximum 60 characters + final String? biometricSuccess; + + /// Message shown on a button that the user can click to leave the + /// current dialog. + /// Maximum 30 characters. + final String? cancelButton; + + /// Message shown as a title in a dialog which indicates the user + /// has not set up credentials authentication on their device. + /// Maximum 60 characters. + final String? deviceCredentialsRequiredTitle; + + /// Message advising the user to go to the settings and configure + /// device credentials on their device. + final String? deviceCredentialsSetupDescription; + + /// Message shown on a button that the user can click to go to settings pages + /// from the current dialog. + /// Maximum 30 characters. + final String? goToSettingsButton; + + /// Message advising the user to go to the settings and configure + /// biometric on their device. + final String? goToSettingsDescription; + + /// Message shown as a title in a dialog which indicates the user + /// that they need to scan biometric to continue. + /// Maximum 60 characters. + final String? signInTitle; + + @override + Map get args { + return { + 'biometricHint': biometricHint ?? androidBiometricHint, + 'biometricNotRecognized': + biometricNotRecognized ?? androidBiometricNotRecognized, + 'biometricSuccess': biometricSuccess ?? androidBiometricSuccess, + 'biometricRequired': + biometricRequiredTitle ?? androidBiometricRequiredTitle, + 'cancelButton': cancelButton ?? androidCancelButton, + 'deviceCredentialsRequired': deviceCredentialsRequiredTitle ?? + androidDeviceCredentialsRequiredTitle, + 'deviceCredentialsSetupDescription': deviceCredentialsSetupDescription ?? + androidDeviceCredentialsSetupDescription, + 'goToSetting': goToSettingsButton ?? goToSettings, + 'goToSettingDescription': + goToSettingsDescription ?? androidGoToSettingsDescription, + 'signInTitle': signInTitle ?? androidSignInTitle, + }; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AndroidAuthMessages && + runtimeType == other.runtimeType && + biometricHint == other.biometricHint && + biometricNotRecognized == other.biometricNotRecognized && + biometricRequiredTitle == other.biometricRequiredTitle && + biometricSuccess == other.biometricSuccess && + cancelButton == other.cancelButton && + deviceCredentialsRequiredTitle == + other.deviceCredentialsRequiredTitle && + deviceCredentialsSetupDescription == + other.deviceCredentialsSetupDescription && + goToSettingsButton == other.goToSettingsButton && + goToSettingsDescription == other.goToSettingsDescription && + signInTitle == other.signInTitle; + + @override + int get hashCode => Object.hash( + super.hashCode, + biometricHint, + biometricNotRecognized, + biometricRequiredTitle, + biometricSuccess, + cancelButton, + deviceCredentialsRequiredTitle, + deviceCredentialsSetupDescription, + goToSettingsButton, + goToSettingsDescription, + signInTitle); +} + +// Default strings for AndroidAuthMessages. Currently supports English. +// Intl.message must be string literals. + +/// Message shown on a button that the user can click to go to settings pages +/// from the current dialog. +String get goToSettings => Intl.message('Go to settings', + desc: 'Message shown on a button that the user can click to go to ' + 'settings pages from the current dialog. Maximum 30 characters.'); + +/// Hint message advising the user how to authenticate with biometrics. +String get androidBiometricHint => Intl.message('Verify identity', + desc: 'Hint message advising the user how to authenticate with biometrics. ' + 'Maximum 60 characters.'); + +/// Message to let the user know that authentication was failed. +String get androidBiometricNotRecognized => + Intl.message('Not recognized. Try again.', + desc: 'Message to let the user know that authentication was failed. ' + 'Maximum 60 characters.'); + +/// Message to let the user know that authentication was successful. It +String get androidBiometricSuccess => Intl.message('Success', + desc: 'Message to let the user know that authentication was successful. ' + 'Maximum 60 characters.'); + +/// Message shown on a button that the user can click to leave the +/// current dialog. +String get androidCancelButton => Intl.message('Cancel', + desc: 'Message shown on a button that the user can click to leave the ' + 'current dialog. Maximum 30 characters.'); + +/// Message shown as a title in a dialog which indicates the user +/// that they need to scan biometric to continue. +String get androidSignInTitle => Intl.message('Authentication required', + desc: 'Message shown as a title in a dialog which indicates the user ' + 'that they need to scan biometric to continue. Maximum 60 characters.'); + +/// Message shown as a title in a dialog which indicates the user +/// has not set up biometric authentication on their device. +String get androidBiometricRequiredTitle => Intl.message('Biometric required', + desc: 'Message shown as a title in a dialog which indicates the user ' + 'has not set up biometric authentication on their device. ' + 'Maximum 60 characters.'); + +/// Message shown as a title in a dialog which indicates the user +/// has not set up credentials authentication on their device. +String get androidDeviceCredentialsRequiredTitle => + Intl.message('Device credentials required', + desc: 'Message shown as a title in a dialog which indicates the user ' + 'has not set up credentials authentication on their device. ' + 'Maximum 60 characters.'); + +/// Message advising the user to go to the settings and configure +/// device credentials on their device. +String get androidDeviceCredentialsSetupDescription => + Intl.message('Device credentials required', + desc: 'Message advising the user to go to the settings and configure ' + 'device credentials on their device.'); + +/// Message advising the user to go to the settings and configure +/// biometric on their device. +String get androidGoToSettingsDescription => Intl.message( + 'Biometric authentication is not set up on your device. Go to ' + "'Settings > Security' to add biometric authentication.", + desc: 'Message advising the user to go to the settings and configure ' + 'biometric on their device.'); diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml new file mode 100644 index 000000000000..99e9e2c547cc --- /dev/null +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -0,0 +1,29 @@ +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.14 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: local_auth + platforms: + android: + package: io.flutter.plugins.localauth + pluginClass: LocalAuthPlugin + dartPluginClass: LocalAuthAndroid + +dependencies: + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.1 + intl: ^0.17.0 + local_auth_platform_interface: ^1.0.1 + +dev_dependencies: + flutter_test: + sdk: flutter 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 new file mode 100644 index 000000000000..86e5713f4bd6 --- /dev/null +++ b/packages/local_auth/local_auth_android/test/local_auth_test.dart @@ -0,0 +1,176 @@ +// 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'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:local_auth_android/local_auth_android.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('LocalAuth', () { + const MethodChannel channel = MethodChannel( + 'plugins.flutter.io/local_auth_android', + ); + + final List log = []; + late LocalAuthAndroid localAuthentication; + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) { + log.add(methodCall); + switch (methodCall.method) { + case 'getEnrolledBiometrics': + return Future>.value(['weak', 'strong']); + default: + return Future.value(true); + } + }); + localAuthentication = LocalAuthAndroid(); + log.clear(); + }); + + test('deviceSupportsBiometrics calls platform', () async { + final bool result = await localAuthentication.deviceSupportsBiometrics(); + + expect( + log, + [ + isMethodCall('deviceSupportsBiometrics', arguments: null), + ], + ); + expect(result, true); + }); + + test('getEnrolledBiometrics calls platform', () async { + final List result = + await localAuthentication.getEnrolledBiometrics(); + + expect( + log, + [ + isMethodCall('getEnrolledBiometrics', arguments: null), + ], + ); + expect(result, [ + BiometricType.weak, + BiometricType.strong, + ]); + }); + + test('isDeviceSupported calls platform', () async { + await localAuthentication.isDeviceSupported(); + expect( + log, + [ + isMethodCall('isDeviceSupported', arguments: null), + ], + ); + }); + + test('stopAuthentication calls platform', () async { + await localAuthentication.stopAuthentication(); + expect( + log, + [ + isMethodCall('stopAuthentication', arguments: null), + ], + ); + }); + + group('With device auth fail over', () { + test('authenticate with no args.', () async { + await localAuthentication.authenticate( + authMessages: [const AndroidAuthMessages()], + localizedReason: 'Needs secure', + options: const AuthenticationOptions(biometricOnly: true), + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': true, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); + + test('authenticate with no sensitive transaction.', () async { + await localAuthentication.authenticate( + authMessages: [const AndroidAuthMessages()], + localizedReason: 'Insecure', + options: const AuthenticationOptions( + sensitiveTransaction: false, + useErrorDialogs: false, + biometricOnly: true, + ), + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': false, + 'biometricOnly': true, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); + }); + + group('With biometrics only', () { + test('authenticate with no args.', () async { + await localAuthentication.authenticate( + authMessages: [const AndroidAuthMessages()], + localizedReason: 'Needs secure', + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); + + test('authenticate with no sensitive transaction.', () async { + await localAuthentication.authenticate( + authMessages: [const AndroidAuthMessages()], + localizedReason: 'Insecure', + options: const AuthenticationOptions( + sensitiveTransaction: false, + useErrorDialogs: false, + ), + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': false, + 'biometricOnly': false, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); + }); + }); +} diff --git a/packages/local_auth/local_auth_ios/AUTHORS b/packages/local_auth/local_auth_ios/AUTHORS new file mode 100644 index 000000000000..d5694690c247 --- /dev/null +++ b/packages/local_auth/local_auth_ios/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Bodhi Mulders diff --git a/packages/local_auth/local_auth_ios/CHANGELOG.md b/packages/local_auth/local_auth_ios/CHANGELOG.md new file mode 100644 index 000000000000..e67f2a4e2ef1 --- /dev/null +++ b/packages/local_auth/local_auth_ios/CHANGELOG.md @@ -0,0 +1,48 @@ +## 1.0.10 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. + +## 1.0.9 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 1.0.8 + +* Updates `local_auth_platform_interface` constraint to the correct minimum + version. + +## 1.0.7 + +* Updates references to the obsolete master branch. + +## 1.0.6 + +* Suppresses warnings for pre-iOS-11 codepaths. + +## 1.0.5 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 1.0.4 + +* Fixes `deviceSupportsBiometrics` to return true when biometric hardware + is available but not enrolled. + +## 1.0.3 + +* Adopts `Object.hash`. + +## 1.0.2 + +* Adds support `localizedFallbackTitle` in authenticateWithBiometrics on iOS. + +## 1.0.1 + +* BREAKING CHANGE: Changes `stopAuthentication` to always return false instead of throwing an error. + +## 1.0.0 + +* Initial release from migration to federated architecture. diff --git a/packages/local_auth/local_auth_ios/LICENSE b/packages/local_auth/local_auth_ios/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/local_auth/local_auth_ios/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/local_auth/local_auth_ios/README.md b/packages/local_auth/local_auth_ios/README.md new file mode 100644 index 000000000000..d9f40436b617 --- /dev/null +++ b/packages/local_auth/local_auth_ios/README.md @@ -0,0 +1,11 @@ +# local\_auth\_ios + +The iOS implementation of [`local_auth`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `local_auth` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/local_auth +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/local_auth/local_auth_ios/example/README.md b/packages/local_auth/local_auth_ios/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/local_auth/local_auth_ios/example/integration_test/local_auth_test.dart b/packages/local_auth/local_auth_ios/example/integration_test/local_auth_test.dart new file mode 100644 index 000000000000..d73cfd6aa625 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/integration_test/local_auth_test.dart @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:local_auth_ios/local_auth_ios.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('canCheckBiometrics', (WidgetTester tester) async { + expect( + LocalAuthIOS().getEnrolledBiometrics(), + completion(isList), + ); + }); +} diff --git a/packages/local_auth/local_auth_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/local_auth/local_auth_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 9.0 + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Flutter/Debug.xcconfig b/packages/local_auth/local_auth_ios/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..e8efba114687 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/local_auth/local_auth_ios/example/ios/Flutter/Release.xcconfig b/packages/local_auth/local_auth_ios/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..399e9340e6f6 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/local_auth/local_auth_ios/example/ios/Podfile b/packages/local_auth/local_auth_ios/example/ios/Podfile new file mode 100644 index 000000000000..ee8f1d9ec3ef --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + + pod 'OCMock','3.5' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..cbf16eef4060 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,630 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 0CCCD07A2CE24E13C9C1EEA4 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3398D2E426164AD8005A052F /* FLTLocalAuthPluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3398D2E326164AD8005A052F /* FLTLocalAuthPluginTests.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 691CB38B382734AF80FBCA4C /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBFA21B380E07A3A585383D /* libPods-RunnerTests.a */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 3398D2D226163948005A052F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3398D2CD26163948005A052F /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3398D2D126163948005A052F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3398D2DC261649CD005A052F /* liblocal_auth.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = liblocal_auth.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 3398D2DF26164A03005A052F /* liblocal_auth.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = liblocal_auth.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 3398D2E326164AD8005A052F /* FLTLocalAuthPluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTLocalAuthPluginTests.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 658CDD04B21E4EA92F8EF229 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 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 = ""; }; + 8D6545CD14E27D6F8299FFD5 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + ADBFA21B380E07A3A585383D /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + D9B3BCBC68F8928E2907FB87 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + EB36DF6C3F25E00DF4175422 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3398D2CA26163948005A052F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 691CB38B382734AF80FBCA4C /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0CCCD07A2CE24E13C9C1EEA4 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BF11D226680B2E002967F3 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 3398D2E326164AD8005A052F /* FLTLocalAuthPluginTests.m */, + 3398D2D126163948005A052F /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 33BF11D226680B2E002967F3 /* RunnerTests */, + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + F8CC53B854B121315C7319D2 /* Pods */, + E2D5FA899A019BD3E0DB0917 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 3398D2CD26163948005A052F /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + E2D5FA899A019BD3E0DB0917 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3398D2DF26164A03005A052F /* liblocal_auth.a */, + 3398D2DC261649CD005A052F /* liblocal_auth.a */, + 9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */, + ADBFA21B380E07A3A585383D /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + F8CC53B854B121315C7319D2 /* Pods */ = { + isa = PBXGroup; + children = ( + EB36DF6C3F25E00DF4175422 /* Pods-Runner.debug.xcconfig */, + 658CDD04B21E4EA92F8EF229 /* Pods-Runner.release.xcconfig */, + 8D6545CD14E27D6F8299FFD5 /* Pods-RunnerTests.debug.xcconfig */, + D9B3BCBC68F8928E2907FB87 /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3398D2CC26163948005A052F /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3398D2D426163948005A052F /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 9C21D3AD392EA849AEB09231 /* [CP] Check Pods Manifest.lock */, + 3398D2C926163948005A052F /* Sources */, + 3398D2CA26163948005A052F /* Frameworks */, + 3398D2CB26163948005A052F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3398D2D326163948005A052F /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 3398D2CD26163948005A052F /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 98D96A2D1A74AF66E3DD2DBC /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 3398D2CC26163948005A052F = { + CreatedOnToolsVersion = 12.4; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 3398D2CC26163948005A052F /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3398D2CB26163948005A052F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + 98D96A2D1A74AF66E3DD2DBC /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9C21D3AD392EA849AEB09231 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3398D2C926163948005A052F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3398D2E426164AD8005A052F /* FLTLocalAuthPluginTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 3398D2D326163948005A052F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 3398D2D226163948005A052F /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 3398D2D526163948005A052F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8D6545CD14E27D6F8299FFD5 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + 3398D2D626163948005A052F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D9B3BCBC68F8928E2907FB87 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.localAuthExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.localAuthExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3398D2D426163948005A052F /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3398D2D526163948005A052F /* Debug */, + 3398D2D626163948005A052F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..b2af55dd6d37 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.h b/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.h @@ -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 +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.m b/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.m @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d22f10b2ab63 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..ebf48f603974 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist b/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..f8e0356d0a68 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + local_auth_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSFaceIDUsageDescription + App needs to authenticate using faces. + + diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/main.m b/packages/local_auth/local_auth_ios/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/main.m @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m similarity index 56% rename from packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m rename to packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m index 3572524d8991..50dbb1a6907b 100644 --- a/packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m +++ b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m @@ -10,7 +10,7 @@ #if __has_include() #import #else -@import local_auth; +@import local_auth_ios; #endif // Private API needed for tests. @@ -269,4 +269,209 @@ - (void)testSkippedLocalizedFallbackTitle { [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } +- (void)testDeviceSupportsBiometrics_withEnrolledHardware { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testDeviceSupportsBiometrics_withNonEnrolledHardware_iOS11 { + if (@available(iOS 11, *)) { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { + // Write error + NSError *__autoreleasing *authError; + [invocation getArgument:&authError atIndex:3]; + *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; + // Write return value + BOOL returnValue = NO; + NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; + [invocation setReturnValue:&nsReturnValue]; + }; + OCMStub([mockAuthContext canEvaluatePolicy:policy + error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) + .andDo(canEvaluatePolicyHandler); + + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertTrue([result boolValue]); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } +} + +- (void)testDeviceSupportsBiometrics_withNoBiometricHardware { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { + // Write error + NSError *__autoreleasing *authError; + [invocation getArgument:&authError atIndex:3]; + *authError = [NSError errorWithDomain:@"error" code:0 userInfo:nil]; + // Write return value + BOOL returnValue = NO; + NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; + [invocation setReturnValue:&nsReturnValue]; + }; + OCMStub([mockAuthContext canEvaluatePolicy:policy + error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) + .andDo(canEvaluatePolicyHandler); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + XCTAssertFalse([result boolValue]); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testGetEnrolledBiometrics_withFaceID_iOS11 { + if (@available(iOS 11, *)) { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeFaceID); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 1); + XCTAssertEqualObjects(result[0], @"face"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } +} + +- (void)testGetEnrolledBiometrics_withTouchID_iOS11 { + if (@available(iOS 11, *)) { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeTouchID); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 1); + XCTAssertEqualObjects(result[0], @"fingerprint"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } +} + +- (void)testGetEnrolledBiometrics_withTouchID_preIOS11 { + if (@available(iOS 11, *)) { + return; + } + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 1); + XCTAssertEqualObjects(result[0], @"fingerprint"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testGetEnrolledBiometrics_withoutEnrolledHardware_iOS11 { + if (@available(iOS 11, *)) { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { + // Write error + NSError *__autoreleasing *authError; + [invocation getArgument:&authError atIndex:3]; + *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; + // Write return value + BOOL returnValue = NO; + NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; + [invocation setReturnValue:&nsReturnValue]; + }; + OCMStub([mockAuthContext canEvaluatePolicy:policy + error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) + .andDo(canEvaluatePolicyHandler); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics" + arguments:@{}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSArray class]]); + XCTAssertEqual([result count], 0); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + } +} @end diff --git a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/Info.plist b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/local_auth/local_auth_ios/example/lib/main.dart b/packages/local_auth/local_auth_ios/example/lib/main.dart new file mode 100644 index 000000000000..fdbf11ffbcaa --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/lib/main.dart @@ -0,0 +1,238 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + _SupportState _supportState = _SupportState.unknown; + bool? _canCheckBiometrics; + List? _enrolledBiometrics; + String _authorized = 'Not Authorized'; + bool _isAuthenticating = false; + + @override + void initState() { + super.initState(); + LocalAuthPlatform.instance.isDeviceSupported().then( + (bool isSupported) => setState(() => _supportState = isSupported + ? _SupportState.supported + : _SupportState.unsupported), + ); + } + + Future _checkBiometrics() async { + late bool deviceSupportsBiometrics; + try { + deviceSupportsBiometrics = + await LocalAuthPlatform.instance.deviceSupportsBiometrics(); + } on PlatformException catch (e) { + deviceSupportsBiometrics = false; + print(e); + } + if (!mounted) { + return; + } + + setState(() { + _canCheckBiometrics = deviceSupportsBiometrics; + }); + } + + Future _getEnrolledBiometrics() async { + late List enrolledBiometrics; + try { + enrolledBiometrics = + await LocalAuthPlatform.instance.getEnrolledBiometrics(); + } on PlatformException catch (e) { + enrolledBiometrics = []; + print(e); + } + if (!mounted) { + return; + } + + setState(() { + _enrolledBiometrics = enrolledBiometrics; + }); + } + + Future _authenticate() async { + bool authenticated = false; + try { + setState(() { + _isAuthenticating = true; + _authorized = 'Authenticating'; + }); + authenticated = await LocalAuthPlatform.instance.authenticate( + localizedReason: 'Let OS determine authentication method', + authMessages: [const IOSAuthMessages()], + options: const AuthenticationOptions( + stickyAuth: true, + ), + ); + setState(() { + _isAuthenticating = false; + }); + } on PlatformException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = 'Error - ${e.message}'; + }); + return; + } + if (!mounted) { + return; + } + + setState( + () => _authorized = authenticated ? 'Authorized' : 'Not Authorized'); + } + + Future _authenticateWithBiometrics() async { + bool authenticated = false; + try { + setState(() { + _isAuthenticating = true; + _authorized = 'Authenticating'; + }); + authenticated = await LocalAuthPlatform.instance.authenticate( + localizedReason: + 'Scan your fingerprint (or face or whatever) to authenticate', + authMessages: [const IOSAuthMessages()], + 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); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: ListView( + padding: const EdgeInsets.only(top: 30), + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_supportState == _SupportState.unknown) + const CircularProgressIndicator() + else if (_supportState == _SupportState.supported) + const Text('This device is supported') + else + const Text('This device is not supported'), + const Divider(height: 100), + Text('Device supports biometrics: $_canCheckBiometrics\n'), + ElevatedButton( + onPressed: _checkBiometrics, + child: const Text('Check biometrics'), + ), + const Divider(height: 100), + Text('Enrolled biometrics: $_enrolledBiometrics\n'), + ElevatedButton( + onPressed: _getEnrolledBiometrics, + child: const Text('Get enrolled biometrics'), + ), + const Divider(height: 100), + Text('Current State: $_authorized\n'), + if (_isAuthenticating) + ElevatedButton( + onPressed: _cancelAuthentication, + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Text('Cancel Authentication'), + Icon(Icons.cancel), + ], + ), + ) + else + Column( + children: [ + ElevatedButton( + onPressed: _authenticate, + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Text('Authenticate'), + Icon(Icons.perm_device_information), + ], + ), + ), + ElevatedButton( + onPressed: _authenticateWithBiometrics, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_isAuthenticating + ? 'Cancel' + : 'Authenticate: biometrics only'), + const Icon(Icons.fingerprint), + ], + ), + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} + +enum _SupportState { + unknown, + supported, + unsupported, +} diff --git a/packages/local_auth/local_auth_ios/example/pubspec.yaml b/packages/local_auth/local_auth_ios/example/pubspec.yaml new file mode 100644 index 000000000000..720d5a732bd5 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/pubspec.yaml @@ -0,0 +1,30 @@ +name: local_auth_ios_example +description: Demonstrates how to use the local_auth_ios plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + flutter: + sdk: flutter + local_auth_ios: + # When depending on this package from a real application you should use: + # local_auth: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + local_auth_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/local_auth/local_auth_ios/example/test_driver/integration_test.dart b/packages/local_auth/local_auth_ios/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/test_driver/integration_test.dart @@ -0,0 +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. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/local_auth/local_auth_ios/ios/Assets/.gitkeep b/packages/local_auth/local_auth_ios/ios/Assets/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.h b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.h similarity index 100% rename from packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.h rename to packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.h diff --git a/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m similarity index 77% rename from packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m rename to packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m index 70113efa00a0..8f61fecfd814 100644 --- a/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -20,7 +20,7 @@ @implementation FLTLocalAuthPlugin { + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/local_auth" + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/local_auth_ios" binaryMessenger:[registrar messenger]]; FLTLocalAuthPlugin *instance = [[FLTLocalAuthPlugin alloc] init]; [registrar addMethodCallDelegate:instance channel:channel]; @@ -35,8 +35,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else { [self authenticate:call.arguments withFlutterResult:result]; } - } else if ([@"getAvailableBiometrics" isEqualToString:call.method]) { - [self getAvailableBiometrics:result]; + } else if ([@"getEnrolledBiometrics" isEqualToString:call.method]) { + [self getEnrolledBiometrics:result]; + } else if ([@"deviceSupportsBiometrics" isEqualToString:call.method]) { + [self deviceSupportsBiometrics:result]; } else if ([@"isDeviceSupported" isEqualToString:call.method]) { result(@YES); } else { @@ -82,7 +84,16 @@ - (void)alertMessage:(NSString *)message handler:^(UIAlertAction *action) { if (UIApplicationOpenSettingsURLString != NULL) { NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; - [[UIApplication sharedApplication] openURL:url]; + if (@available(iOS 10, *)) { + [[UIApplication sharedApplication] openURL:url + options:@{} + completionHandler:NULL]; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [[UIApplication sharedApplication] openURL:url]; +#pragma clang diagnostic pop + } result(@NO); } }]; @@ -93,14 +104,44 @@ - (void)alertMessage:(NSString *)message completion:nil]; } -- (void)getAvailableBiometrics:(FlutterResult)result { +- (void)deviceSupportsBiometrics:(FlutterResult)result { + LAContext *context = self.createAuthContext; + NSError *authError = nil; + // Check if authentication with biometrics is possible. + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + error:&authError]) { + if (authError == nil) { + result(@YES); + return; + } + } + // If not, check if it is because no biometrics are enrolled (but still present). + if (authError != nil) { + if (@available(iOS 11, *)) { + if (authError.code == LAErrorBiometryNotEnrolled) { + result(@YES); + return; + } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + } else if (authError.code == LAErrorTouchIDNotEnrolled) { + result(@YES); + return; +#pragma clang diagnostic pop + } + } + + result(@NO); +} + +- (void)getEnrolledBiometrics:(FlutterResult)result { LAContext *context = self.createAuthContext; NSError *authError = nil; NSMutableArray *biometrics = [[NSMutableArray alloc] init]; if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) { if (authError == nil) { - if (@available(iOS 11.0.1, *)) { + if (@available(iOS 11, *)) { if (context.biometryType == LABiometryTypeFaceID) { [biometrics addObject:@"face"]; } else if (context.biometryType == LABiometryTypeTouchID) { @@ -110,8 +151,6 @@ - (void)getAvailableBiometrics:(FlutterResult)result { [biometrics addObject:@"fingerprint"]; } } - } else if (authError.code == LAErrorTouchIDNotEnrolled) { - [biometrics addObject:@"undefined"]; } result(biometrics); } @@ -178,10 +217,15 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success } else { switch (error.code) { case LAErrorPasscodeNotSet: +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in these constants when + // iOS 10 support is dropped. The values are the same, only the names have changed. case LAErrorTouchIDNotAvailable: case LAErrorTouchIDNotEnrolled: - case LAErrorUserFallback: case LAErrorTouchIDLockout: +#pragma clang diagnostic pop + case LAErrorUserFallback: [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; return; case LAErrorSystemCancel: @@ -201,7 +245,12 @@ - (void)handleErrors:(NSError *)authError NSString *errorCode = @"NotAvailable"; switch (authError.code) { case LAErrorPasscodeNotSet: +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in this constant when + // iOS 10 support is dropped. The values are the same, only the names have changed. case LAErrorTouchIDNotEnrolled: +#pragma clang diagnostic pop if ([arguments[@"useErrorDialogs"] boolValue]) { [self alertMessage:arguments[@"goToSettingDescriptionIOS"] firstButton:arguments[@"okButton"] @@ -211,7 +260,12 @@ - (void)handleErrors:(NSError *)authError } errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; break; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in this constant when + // iOS 10 support is dropped. The values are the same, only the names have changed. case LAErrorTouchIDLockout: +#pragma clang diagnostic pop [self alertMessage:arguments[@"lockOut"] firstButton:arguments[@"okButton"] flutterResult:result diff --git a/packages/local_auth/local_auth/ios/local_auth.podspec b/packages/local_auth/local_auth_ios/ios/local_auth_ios.podspec similarity index 89% rename from packages/local_auth/local_auth/ios/local_auth.podspec rename to packages/local_auth/local_auth_ios/ios/local_auth_ios.podspec index 4ab779ad50cd..0828c6085ea2 100644 --- a/packages/local_auth/local_auth/ios/local_auth.podspec +++ b/packages/local_auth/local_auth_ios/ios/local_auth_ios.podspec @@ -2,7 +2,7 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| - s.name = 'local_auth' + s.name = 'local_auth_ios' s.version = '0.0.1' s.summary = 'Flutter Local Auth' s.description = <<-DESC @@ -13,7 +13,7 @@ Downloaded by pub (not CocoaPods). s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/local_auth' } - s.documentation_url = 'https://pub.dev/packages/local_auth' + s.documentation_url = 'https://pub.dev/packages/local_auth_ios' s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' diff --git a/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart b/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart new file mode 100644 index 000000000000..217fd39d9901 --- /dev/null +++ b/packages/local_auth/local_auth_ios/lib/local_auth_ios.dart @@ -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. + +import 'package:flutter/services.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; + +import 'types/auth_messages_ios.dart'; + +export 'package:local_auth_ios/types/auth_messages_ios.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'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/local_auth_ios'); + +/// The implementation of [LocalAuthPlatform] for iOS. +class LocalAuthIOS extends LocalAuthPlatform { + /// Registers this class as the default instance of [LocalAuthPlatform]. + static void registerWith() { + LocalAuthPlatform.instance = LocalAuthIOS(); + } + + @override + Future authenticate({ + required String localizedReason, + required Iterable authMessages, + 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 IOSAuthMessages().args); + for (final AuthMessages messages in authMessages) { + if (messages is IOSAuthMessages) { + args.addAll(messages.args); + } + } + return (await _channel.invokeMethod('authenticate', args)) ?? false; + } + + @override + Future deviceSupportsBiometrics() async { + return (await _channel.invokeMethod('deviceSupportsBiometrics')) ?? + false; + } + + @override + Future> getEnrolledBiometrics() async { + final List result = (await _channel.invokeListMethod( + 'getEnrolledBiometrics', + )) ?? + []; + final List biometrics = []; + for (final String value in result) { + switch (value) { + case 'face': + biometrics.add(BiometricType.face); + break; + case 'fingerprint': + biometrics.add(BiometricType.fingerprint); + break; + case 'iris': + biometrics.add(BiometricType.iris); + break; + } + } + return biometrics; + } + + @override + Future isDeviceSupported() async => + (await _channel.invokeMethod('isDeviceSupported')) ?? false; + + /// Always returns false as this method is not supported on iOS. + @override + Future stopAuthentication() async { + return false; + } +} diff --git a/packages/local_auth/local_auth_ios/lib/types/auth_messages_ios.dart b/packages/local_auth/local_auth_ios/lib/types/auth_messages_ios.dart new file mode 100644 index 000000000000..e5173fc4ab4f --- /dev/null +++ b/packages/local_auth/local_auth_ios/lib/types/auth_messages_ios.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 'package:flutter/foundation.dart'; +import 'package:intl/intl.dart'; +import 'package:local_auth_platform_interface/types/auth_messages.dart'; + +/// Class wrapping all authentication messages needed on iOS. +/// Provides default values for all messages. +@immutable +class IOSAuthMessages extends AuthMessages { + /// Constructs a new instance. + const IOSAuthMessages({ + this.lockOut, + this.goToSettingsButton, + this.goToSettingsDescription, + this.cancelButton, + this.localizedFallbackTitle, + }); + + /// Message advising the user to re-enable biometrics on their device. + final String? lockOut; + + /// Message shown on a button that the user can click to go to settings pages + /// from the current dialog. + /// Maximum 30 characters. + final String? goToSettingsButton; + + /// Message advising the user to go to the settings and configure Biometrics + /// for their device. + final String? goToSettingsDescription; + + /// Message shown on a button that the user can click to leave the current + /// dialog. + /// Maximum 30 characters. + final String? cancelButton; + + /// The localized title for the fallback button in the dialog presented to + /// the user during authentication. + final String? localizedFallbackTitle; + + @override + Map get args { + return { + 'lockOut': lockOut ?? iOSLockOut, + 'goToSetting': goToSettingsButton ?? goToSettings, + 'goToSettingDescriptionIOS': + goToSettingsDescription ?? iOSGoToSettingsDescription, + 'okButton': cancelButton ?? iOSOkButton, + if (localizedFallbackTitle != null) + 'localizedFallbackTitle': localizedFallbackTitle!, + }; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is IOSAuthMessages && + runtimeType == other.runtimeType && + lockOut == other.lockOut && + goToSettingsButton == other.goToSettingsButton && + goToSettingsDescription == other.goToSettingsDescription && + cancelButton == other.cancelButton && + localizedFallbackTitle == other.localizedFallbackTitle; + + @override + int get hashCode => Object.hash( + super.hashCode, + lockOut, + goToSettingsButton, + goToSettingsDescription, + cancelButton, + localizedFallbackTitle, + ); +} + +// Default Strings for IOSAuthMessages plugin. Currently supports English. +// Intl.message must be string literals. + +/// Message shown on a button that the user can click to go to settings pages +/// from the current dialog. +String get goToSettings => Intl.message('Go to settings', + desc: 'Message shown on a button that the user can click to go to ' + 'settings pages from the current dialog. Maximum 30 characters.'); + +/// Message advising the user to re-enable biometrics on their device. +/// It shows in a dialog on iOS. +String get iOSLockOut => Intl.message( + 'Biometric authentication is disabled. Please lock and unlock your screen to ' + 'enable it.', + desc: 'Message advising the user to re-enable biometrics on their device.'); + +/// Message advising the user to go to the settings and configure Biometrics +/// for their device. +String get iOSGoToSettingsDescription => Intl.message( + 'Biometric authentication is not set up on your device. Please either enable ' + 'Touch ID or Face ID on your phone.', + desc: + 'Message advising the user to go to the settings and configure Biometrics ' + 'for their device.'); + +/// Message shown on a button that the user can click to leave the current +/// dialog. +String get iOSOkButton => Intl.message('OK', + desc: 'Message showed on a button that the user can click to leave the ' + 'current dialog. Maximum 30 characters.'); diff --git a/packages/local_auth/local_auth_ios/pubspec.yaml b/packages/local_auth/local_auth_ios/pubspec.yaml new file mode 100644 index 000000000000..9cdeef963c34 --- /dev/null +++ b/packages/local_auth/local_auth_ios/pubspec.yaml @@ -0,0 +1,27 @@ +name: local_auth_ios +description: iOS implementation of the local_auth plugin. +repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth_ios +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 +version: 1.0.10 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: local_auth + platforms: + ios: + pluginClass: FLTLocalAuthPlugin + dartPluginClass: LocalAuthIOS + +dependencies: + flutter: + sdk: flutter + intl: ^0.17.0 + local_auth_platform_interface: ^1.0.1 + +dev_dependencies: + flutter_test: + sdk: flutter 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 new file mode 100644 index 000000000000..0ad89e52f5ce --- /dev/null +++ b/packages/local_auth/local_auth_ios/test/local_auth_test.dart @@ -0,0 +1,183 @@ +// 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'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('LocalAuth', () { + const MethodChannel channel = MethodChannel( + 'plugins.flutter.io/local_auth_ios', + ); + + final List log = []; + late LocalAuthIOS localAuthentication; + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) { + log.add(methodCall); + switch (methodCall.method) { + case 'getEnrolledBiometrics': + return Future>.value( + ['face', 'fingerprint', 'iris', 'undefined']); + default: + return Future.value(true); + } + }); + localAuthentication = LocalAuthIOS(); + log.clear(); + }); + + test('deviceSupportsBiometrics calls platform', () async { + final bool result = await localAuthentication.deviceSupportsBiometrics(); + + expect( + log, + [ + isMethodCall('deviceSupportsBiometrics', arguments: null), + ], + ); + expect(result, true); + }); + + test('getEnrolledBiometrics calls platform', () async { + final List result = + await localAuthentication.getEnrolledBiometrics(); + + expect( + log, + [ + isMethodCall('getEnrolledBiometrics', arguments: null), + ], + ); + expect(result, [ + BiometricType.face, + BiometricType.fingerprint, + BiometricType.iris + ]); + }); + + test('isDeviceSupported calls platform', () async { + await localAuthentication.isDeviceSupported(); + + expect( + log, + [ + isMethodCall('isDeviceSupported', arguments: null), + ], + ); + }); + + test('stopAuthentication returns false', () async { + final bool result = await localAuthentication.stopAuthentication(); + expect(result, false); + }); + + group('With device auth fail over', () { + test('authenticate with no args.', () async { + await localAuthentication.authenticate( + authMessages: [const IOSAuthMessages()], + localizedReason: 'Needs secure', + options: const AuthenticationOptions(biometricOnly: true), + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': true, + }..addAll(const IOSAuthMessages().args)), + ], + ); + }); + + test('authenticate with no localizedReason.', () async { + await expectLater( + localAuthentication.authenticate( + authMessages: [const IOSAuthMessages()], + localizedReason: '', + options: const AuthenticationOptions(biometricOnly: true), + ), + throwsAssertionError, + ); + }); + }); + + group('With biometrics only', () { + test('authenticate with no args.', () async { + await localAuthentication.authenticate( + authMessages: [const IOSAuthMessages()], + localizedReason: 'Needs secure', + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + }..addAll(const IOSAuthMessages().args)), + ], + ); + }); + + test('authenticate with `localizedFallbackTitle`', () async { + await localAuthentication.authenticate( + authMessages: [ + const IOSAuthMessages(localizedFallbackTitle: 'Enter PIN'), + ], + localizedReason: 'Needs secure', + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + 'localizedFallbackTitle': 'Enter PIN', + }..addAll(const IOSAuthMessages().args)), + ], + ); + }); + + test('authenticate with no sensitive transaction.', () async { + await localAuthentication.authenticate( + authMessages: [const IOSAuthMessages()], + localizedReason: 'Insecure', + options: const AuthenticationOptions( + sensitiveTransaction: false, + useErrorDialogs: false, + ), + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': false, + 'biometricOnly': false, + }..addAll(const IOSAuthMessages().args)), + ], + ); + }); + }); + }); +} diff --git a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md index 0d8803f93540..f0313ce99be6 100644 --- a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md +++ b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md @@ -1,3 +1,27 @@ +## 1.0.5 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. + +## 1.0.4 + +* Updates references to the obsolete master branch. +* Removes unnecessary imports. + +## 1.0.3 + +* Fixes regression in the default method channel implementation of + `deviceSupportsBiometrics` from federation that would cause it to return true + only if something is enrolled. + +## 1.0.2 + +* Adopts `Object.hash`. + +## 1.0.1 + +* Export externally used types from local_auth_platform_interface.dart directly. + ## 1.0.0 * Initial release. diff --git a/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart b/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart index c68a3bfb8371..b3b0a653b514 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart @@ -3,10 +3,7 @@ // found in the LICENSE file. import 'package:flutter/services.dart'; -import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; -import 'package:local_auth_platform_interface/types/auth_messages.dart'; -import 'package:local_auth_platform_interface/types/auth_options.dart'; -import 'package:local_auth_platform_interface/types/biometric_type.dart'; +import 'local_auth_platform_interface.dart'; const MethodChannel _channel = MethodChannel('plugins.flutter.io/local_auth'); @@ -57,6 +54,8 @@ class DefaultLocalAuthPlatform extends LocalAuthPlatform { biometrics.add(BiometricType.iris); break; case 'undefined': + // Sentinel value for the case when nothing is enrolled, but hardware + // support for biometrics is available. break; } } @@ -65,7 +64,14 @@ class DefaultLocalAuthPlatform extends LocalAuthPlatform { @override Future deviceSupportsBiometrics() async { - return (await getEnrolledBiometrics()).isNotEmpty; + final List availableBiometrics = + (await _channel.invokeListMethod( + 'getAvailableBiometrics', + )) ?? + []; + // If anything, including the 'undefined' sentinel, is returned, then there + // is device support for biometrics. + return availableBiometrics.isNotEmpty; } @override diff --git a/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart index b909ee90d12b..4c6d58238edd 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart @@ -2,12 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:local_auth_platform_interface/default_method_channel_platform.dart'; -import 'package:local_auth_platform_interface/types/auth_messages.dart'; -import 'package:local_auth_platform_interface/types/auth_options.dart'; -import 'package:local_auth_platform_interface/types/biometric_type.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'default_method_channel_platform.dart'; +import 'types/types.dart'; + +export 'package:local_auth_platform_interface/types/types.dart'; + /// The interface that implementations of local_auth must implement. /// /// Platform implementations should extend this class rather than implement it as `local_auth` diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart b/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart index c4b646c0b97a..a5af8e73a640 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.dart @@ -52,9 +52,10 @@ class AuthenticationOptions { biometricOnly == other.biometricOnly; @override - int get hashCode => - useErrorDialogs.hashCode ^ - stickyAuth.hashCode ^ - sensitiveTransaction.hashCode ^ - biometricOnly.hashCode; + int get hashCode => Object.hash( + useErrorDialogs, + stickyAuth, + sensitiveTransaction, + biometricOnly, + ); } diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/types.dart b/packages/local_auth/local_auth_platform_interface/lib/types/types.dart new file mode 100644 index 000000000000..ea43b942cffd --- /dev/null +++ b/packages/local_auth/local_auth_platform_interface/lib/types/types.dart @@ -0,0 +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. + +export 'auth_messages.dart'; +export 'auth_options.dart'; +export 'biometric_type.dart'; diff --git a/packages/local_auth/local_auth_platform_interface/pubspec.yaml b/packages/local_auth/local_auth_platform_interface/pubspec.yaml index f04268926ebd..92da218d8be5 100644 --- a/packages/local_auth/local_auth_platform_interface/pubspec.yaml +++ b/packages/local_auth/local_auth_platform_interface/pubspec.yaml @@ -1,14 +1,14 @@ name: local_auth_platform_interface description: A common platform interface for the local_auth plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/local_auth/local_auth_platform_interface +repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth_platform_interface issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%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: 1.0.0 +version: 1.0.5 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -19,4 +19,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - mockito: ^5.0.0 \ No newline at end of file + mockito: ^5.0.0 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 3853fd84c6fc..824597ab2953 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 @@ -2,15 +2,10 @@ // 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_test/flutter_test.dart'; import 'package:local_auth_platform_interface/default_method_channel_platform.dart'; import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; -import 'package:local_auth_platform_interface/types/auth_messages.dart'; -import 'package:local_auth_platform_interface/types/auth_options.dart'; -import 'package:local_auth_platform_interface/types/biometric_type.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -19,9 +14,13 @@ void main() { 'plugins.flutter.io/local_auth', ); - final List log = []; + late List log; late LocalAuthPlatform localAuthentication; + setUp(() async { + log = []; + }); + test( 'DefaultLocalAuthPlatform is registered as the default platform implementation', () async { @@ -32,10 +31,9 @@ void main() { test('getAvailableBiometrics', () async { channel.setMockMethodCallHandler((MethodCall methodCall) { log.add(methodCall); - return Future.value([]); + return Future.value([]); }); localAuthentication = DefaultLocalAuthPlatform(); - log.clear(); await localAuthentication.getEnrolledBiometrics(); expect( log, @@ -45,6 +43,29 @@ void main() { ); }); + test('deviceSupportsBiometrics handles special sentinal value', () async { + // The pre-federation implementation of the platform channels, which the + // default implementation retains compatibility with for the benefit of any + // 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) { + log.add(methodCall); + return Future.value(['undefined']); + }); + + localAuthentication = DefaultLocalAuthPlatform(); + final bool supportsBiometrics = + await localAuthentication.deviceSupportsBiometrics(); + expect(supportsBiometrics, true); + expect( + log, + [ + isMethodCall('getAvailableBiometrics', arguments: null), + ], + ); + }); + group('Boolean returning methods', () { setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) { @@ -52,7 +73,6 @@ void main() { return Future.value(true); }); localAuthentication = DefaultLocalAuthPlatform(); - log.clear(); }); test('isDeviceSupported', () async { diff --git a/packages/local_auth/local_auth_windows/AUTHORS b/packages/local_auth/local_auth_windows/AUTHORS new file mode 100644 index 000000000000..5db3d584e6bc --- /dev/null +++ b/packages/local_auth/local_auth_windows/AUTHORS @@ -0,0 +1,7 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +Alexandre Zollinger Chohfi \ No newline at end of file diff --git a/packages/local_auth/local_auth_windows/CHANGELOG.md b/packages/local_auth/local_auth_windows/CHANGELOG.md new file mode 100644 index 000000000000..b4f2061f2c27 --- /dev/null +++ b/packages/local_auth/local_auth_windows/CHANGELOG.md @@ -0,0 +1,21 @@ +## 1.0.4 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. + +## 1.0.3 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 1.0.2 + +* Updates `local_auth_platform_interface` constraint to the correct minimum + version. + +## 1.0.1 + +* Updates references to the obsolete master branch. + +## 1.0.0 + +* Initial release of Windows support. diff --git a/packages/local_auth/local_auth_windows/LICENSE b/packages/local_auth/local_auth_windows/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/local_auth/local_auth_windows/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/local_auth/local_auth_windows/README.md b/packages/local_auth/local_auth_windows/README.md new file mode 100644 index 000000000000..0c2984f40003 --- /dev/null +++ b/packages/local_auth/local_auth_windows/README.md @@ -0,0 +1,11 @@ +# local\_auth\_windows + +The Windows implementation of [`local_auth`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `local_auth` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/local_auth +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin \ No newline at end of file diff --git a/packages/local_auth/local_auth_windows/example/.gitignore b/packages/local_auth/local_auth_windows/example/.gitignore new file mode 100644 index 000000000000..0fa6b675c0a5 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/local_auth/local_auth_windows/example/.metadata b/packages/local_auth/local_auth_windows/example/.metadata new file mode 100644 index 000000000000..166a9984ca13 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: c860cba910319332564e1e9d470a17074c1f2dfd + channel: stable + +project_type: app diff --git a/packages/local_auth/local_auth_windows/example/README.md b/packages/local_auth/local_auth_windows/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/local_auth/local_auth_windows/example/integration_test/local_auth_test.dart b/packages/local_auth/local_auth_windows/example/integration_test/local_auth_test.dart new file mode 100644 index 000000000000..cedaaf28ff24 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/integration_test/local_auth_test.dart @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:local_auth_windows/local_auth_windows.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('canCheckBiometrics', (WidgetTester tester) async { + expect( + LocalAuthWindows().getEnrolledBiometrics(), + completion(isList), + ); + }); +} diff --git a/packages/local_auth/local_auth_windows/example/lib/main.dart b/packages/local_auth/local_auth_windows/example/lib/main.dart new file mode 100644 index 000000000000..c7b3fd923891 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/lib/main.dart @@ -0,0 +1,239 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; +import 'package:local_auth_windows/local_auth_windows.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + _SupportState _supportState = _SupportState.unknown; + bool? _deviceSupportsBiometrics; + List? _enrolledBiometrics; + String _authorized = 'Not Authorized'; + bool _isAuthenticating = false; + + @override + void initState() { + super.initState(); + LocalAuthPlatform.instance.isDeviceSupported().then( + (bool isSupported) => setState(() => _supportState = isSupported + ? _SupportState.supported + : _SupportState.unsupported), + ); + } + + Future _checkBiometrics() async { + late bool deviceSupportsBiometrics; + try { + deviceSupportsBiometrics = + await LocalAuthPlatform.instance.deviceSupportsBiometrics(); + } on PlatformException catch (e) { + deviceSupportsBiometrics = false; + print(e); + } + if (!mounted) { + return; + } + + setState(() { + _deviceSupportsBiometrics = deviceSupportsBiometrics; + }); + } + + Future _getEnrolledBiometrics() async { + late List availableBiometrics; + try { + availableBiometrics = + await LocalAuthPlatform.instance.getEnrolledBiometrics(); + } on PlatformException catch (e) { + availableBiometrics = []; + print(e); + } + if (!mounted) { + return; + } + + setState(() { + _enrolledBiometrics = availableBiometrics; + }); + } + + Future _authenticate() async { + bool authenticated = false; + try { + setState(() { + _isAuthenticating = true; + _authorized = 'Authenticating'; + }); + authenticated = await LocalAuthPlatform.instance.authenticate( + localizedReason: 'Let OS determine authentication method', + authMessages: [const WindowsAuthMessages()], + options: const AuthenticationOptions( + stickyAuth: true, + ), + ); + setState(() { + _isAuthenticating = false; + }); + } on PlatformException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = 'Error - ${e.message}'; + }); + return; + } + if (!mounted) { + return; + } + + setState( + () => _authorized = authenticated ? 'Authorized' : 'Not Authorized'); + } + + Future _authenticateWithBiometrics() async { + bool authenticated = false; + try { + setState(() { + _isAuthenticating = true; + _authorized = 'Authenticating'; + }); + authenticated = await 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); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: ListView( + padding: const EdgeInsets.only(top: 30), + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_supportState == _SupportState.unknown) + const CircularProgressIndicator() + else if (_supportState == _SupportState.supported) + const Text('This device is supported') + else + const Text('This device is not supported'), + const Divider(height: 100), + Text( + 'Device supports biometrics: $_deviceSupportsBiometrics\n'), + ElevatedButton( + onPressed: _checkBiometrics, + child: const Text('Check biometrics'), + ), + const Divider(height: 100), + Text('Enrolled biometrics: $_enrolledBiometrics\n'), + ElevatedButton( + onPressed: _getEnrolledBiometrics, + child: const Text('Get enrolled biometrics'), + ), + const Divider(height: 100), + Text('Current State: $_authorized\n'), + if (_isAuthenticating) + ElevatedButton( + onPressed: _cancelAuthentication, + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Text('Cancel Authentication'), + Icon(Icons.cancel), + ], + ), + ) + else + Column( + children: [ + ElevatedButton( + onPressed: _authenticate, + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Text('Authenticate'), + Icon(Icons.perm_device_information), + ], + ), + ), + ElevatedButton( + onPressed: _authenticateWithBiometrics, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_isAuthenticating + ? 'Cancel' + : 'Authenticate: biometrics only'), + const Icon(Icons.fingerprint), + ], + ), + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} + +enum _SupportState { + unknown, + supported, + unsupported, +} diff --git a/packages/local_auth/local_auth_windows/example/pubspec.yaml b/packages/local_auth/local_auth_windows/example/pubspec.yaml new file mode 100644 index 000000000000..4bb2671f6826 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/pubspec.yaml @@ -0,0 +1,30 @@ +name: local_auth_windows_example +description: Demonstrates how to use the local_auth_windows plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +dependencies: + flutter: + sdk: flutter + local_auth_platform_interface: ^1.0.0 + local_auth_windows: + # When depending on this package from a real application you should use: + # local_auth_windows: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/local_auth/local_auth_windows/example/test_driver/integration_test.dart b/packages/local_auth/local_auth_windows/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/test_driver/integration_test.dart @@ -0,0 +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. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/local_auth/local_auth_windows/example/windows/.gitignore b/packages/local_auth/local_auth_windows/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/local_auth/local_auth_windows/example/windows/CMakeLists.txt b/packages/local_auth/local_auth_windows/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..2163be881bd2 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/CMakeLists.txt @@ -0,0 +1,100 @@ +cmake_minimum_required(VERSION 3.14) +project(local_auth_windows_example LANGUAGES CXX) + +set(BINARY_NAME "local_auth_windows_example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Enable the test target. +set(include_local_auth_windows_tests TRUE) +# Provide an alias for the test target using the name expected by repo tooling. +add_custom_target(unit_tests DEPENDS local_auth_windows_test) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/local_auth/local_auth_windows/example/windows/flutter/CMakeLists.txt b/packages/local_auth/local_auth_windows/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..b2e4bd8d658b --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/local_auth/local_auth_windows/example/windows/flutter/generated_plugins.cmake b/packages/local_auth/local_auth_windows/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..ef187dcae56f --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + local_auth_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/local_auth/local_auth_windows/example/windows/runner/CMakeLists.txt b/packages/local_auth/local_auth_windows/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..de2d8916b72b --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/local_auth/local_auth_windows/example/windows/runner/Runner.rc b/packages/local_auth/local_auth_windows/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..5fdea291cf19 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/local_auth/local_auth_windows/example/windows/runner/flutter_window.cpp b/packages/local_auth/local_auth_windows/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..8254bd9ff3c1 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/runner/flutter_window.cpp @@ -0,0 +1,65 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/local_auth/local_auth_windows/example/windows/runner/flutter_window.h b/packages/local_auth/local_auth_windows/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..f1fc669093d0 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/runner/flutter_window.h @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/local_auth/local_auth_windows/example/windows/runner/main.cpp b/packages/local_auth/local_auth_windows/example/windows/runner/main.cpp new file mode 100644 index 000000000000..4e37ae286c01 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/runner/main.cpp @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t* command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"local_auth_windows_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/local_auth/local_auth_windows/example/windows/runner/resource.h b/packages/local_auth/local_auth_windows/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/local_auth/local_auth_windows/example/windows/runner/resources/app_icon.ico b/packages/local_auth/local_auth_windows/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/local_auth/local_auth_windows/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/local_auth/local_auth_windows/example/windows/runner/runner.exe.manifest b/packages/local_auth/local_auth_windows/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/local_auth/local_auth_windows/example/windows/runner/utils.cpp b/packages/local_auth/local_auth_windows/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..fb7e945a63b7 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/runner/utils.cpp @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE* unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = + ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, + nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/local_auth/local_auth_windows/example/windows/runner/utils.h b/packages/local_auth/local_auth_windows/example/windows/runner/utils.h new file mode 100644 index 000000000000..bd81e1e02338 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/runner/utils.h @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/local_auth/local_auth_windows/example/windows/runner/win32_window.cpp b/packages/local_auth/local_auth_windows/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..85aa3614e8ad --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/runner/win32_window.cpp @@ -0,0 +1,241 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { ++g_active_window_count; } + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { return window_handle_; } + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/packages/local_auth/local_auth_windows/example/windows/runner/win32_window.h b/packages/local_auth/local_auth_windows/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..d2a730052223 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/windows/runner/win32_window.h @@ -0,0 +1,99 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart b/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart new file mode 100644 index 000000000000..b373782c2187 --- /dev/null +++ b/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart @@ -0,0 +1,82 @@ +// 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'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; +import 'types/auth_messages_windows.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 { + /// Registers this class as the default instance of [LocalAuthPlatform]. + static void registerWith() { + LocalAuthPlatform.instance = LocalAuthWindows(); + } + + @override + Future authenticate({ + required String localizedReason, + required Iterable authMessages, + 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); + } + } + return (await _channel.invokeMethod('authenticate', args)) ?? false; + } + + @override + Future deviceSupportsBiometrics() async { + return (await _channel.invokeMethod('deviceSupportsBiometrics')) ?? + false; + } + + @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; + } + } + return biometrics; + } + + @override + Future isDeviceSupported() async => + (await _channel.invokeMethod('isDeviceSupported')) ?? false; + + /// Always returns false as this method is not supported on Windows. + @override + Future stopAuthentication() async { + return false; + } +} diff --git a/packages/local_auth/local_auth_windows/lib/types/auth_messages_windows.dart b/packages/local_auth/local_auth_windows/lib/types/auth_messages_windows.dart new file mode 100644 index 000000000000..e47e8737153c --- /dev/null +++ b/packages/local_auth/local_auth_windows/lib/types/auth_messages_windows.dart @@ -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. + +import 'package:flutter/foundation.dart'; +import 'package:local_auth_platform_interface/types/auth_messages.dart'; + +/// Windows side authentication messages. +/// +/// Provides default values for all messages. +/// +/// Currently unused. +@immutable +class WindowsAuthMessages extends AuthMessages { + /// Constructs a new instance. + const WindowsAuthMessages(); + + @override + Map get args { + return {}; + } +} diff --git a/packages/local_auth/local_auth_windows/pubspec.yaml b/packages/local_auth/local_auth_windows/pubspec.yaml new file mode 100644 index 000000000000..9a2effed92ee --- /dev/null +++ b/packages/local_auth/local_auth_windows/pubspec.yaml @@ -0,0 +1,26 @@ +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 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.10.0" + +flutter: + plugin: + implements: local_auth + platforms: + windows: + pluginClass: LocalAuthPlugin + dartPluginClass: LocalAuthWindows + +dependencies: + flutter: + sdk: flutter + local_auth_platform_interface: ^1.0.1 + +dev_dependencies: + flutter_test: + sdk: flutter 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 new file mode 100644 index 000000000000..b11c19e7b339 --- /dev/null +++ b/packages/local_auth/local_auth_windows/test/local_auth_test.dart @@ -0,0 +1,79 @@ +// 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'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:local_auth_windows/local_auth_windows.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('authenticate', () { + const MethodChannel channel = MethodChannel( + 'plugins.flutter.io/local_auth_windows', + ); + + final List log = []; + late LocalAuthWindows localAuthentication; + + 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(); + }); + + test('authenticate with no arguments passes expected defaults', () async { + await localAuthentication.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)), + ], + ); + }); + + 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, + ), + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'My localized reason', + 'useErrorDialogs': false, + 'stickyAuth': true, + 'sensitiveTransaction': false, + 'biometricOnly': true, + }..addAll(const WindowsAuthMessages().args)), + ], + ); + }); + }); +} diff --git a/packages/local_auth/local_auth_windows/windows/CMakeLists.txt b/packages/local_auth/local_auth_windows/windows/CMakeLists.txt new file mode 100644 index 000000000000..bcf59bb827c7 --- /dev/null +++ b/packages/local_auth/local_auth_windows/windows/CMakeLists.txt @@ -0,0 +1,120 @@ +cmake_minimum_required(VERSION 3.15) +set(PROJECT_NAME "local_auth_windows") +set(WIL_VERSION "1.0.220201.1") +set(CPPWINRT_VERSION "2.0.220418.1") +project(${PROJECT_NAME} LANGUAGES CXX) +include(FetchContent) + +set(PLUGIN_NAME "${PROJECT_NAME}_plugin") + +FetchContent_Declare(nuget + URL "https://dist.nuget.org/win-x86-commandline/v6.0.0/nuget.exe" + URL_HASH SHA256=04eb6c4fe4213907e2773e1be1bbbd730e9a655a3c9c58387ce8d4a714a5b9e1 + DOWNLOAD_NO_EXTRACT true +) + +find_program(NUGET nuget) +if (NOT NUGET) + message("Nuget.exe not found, trying to download or use cached version.") + FetchContent_MakeAvailable(nuget) + set(NUGET ${nuget_SOURCE_DIR}/nuget.exe) +endif() + +execute_process(COMMAND + ${NUGET} install Microsoft.Windows.ImplementationLibrary -Version ${WIL_VERSION} -OutputDirectory ${CMAKE_BINARY_DIR}/packages + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE ret) +if (NOT ret EQUAL 0) + message(FATAL_ERROR "Failed to install nuget package Microsoft.Windows.ImplementationLibrary.${WIL_VERSION}") +endif() + +execute_process(COMMAND + ${NUGET} install Microsoft.Windows.CppWinRT -Version ${CPPWINRT_VERSION} -OutputDirectory packages + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE ret) +if (NOT ret EQUAL 0) + message(FATAL_ERROR "Failed to install nuget package Microsoft.Windows.CppWinRT.${CPPWINRT_VERSION}") +endif() + +set(CPPWINRT ${CMAKE_BINARY_DIR}/packages/Microsoft.Windows.CppWinRT.${CPPWINRT_VERSION}/bin/cppwinrt.exe) +execute_process(COMMAND + ${CPPWINRT} -input sdk -output include + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE ret) +if (NOT ret EQUAL 0) + message(FATAL_ERROR "Failed to run cppwinrt.exe") +endif() + +include_directories(BEFORE SYSTEM ${CMAKE_BINARY_DIR}/include) + +list(APPEND PLUGIN_SOURCES + "local_auth_plugin.cpp" +) + +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}) +set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) +target_compile_features(${PLUGIN_NAME} PRIVATE cxx_std_20) +target_compile_options(${PLUGIN_NAME} PRIVATE /await) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE ${CMAKE_BINARY_DIR}/packages/Microsoft.Windows.ImplementationLibrary.${WIL_VERSION}/build/native/Microsoft.Windows.ImplementationLibrary.targets) +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin windowsapp) + +# List of absolute paths to libraries that should be bundled with the plugin +set(file_chooser_bundled_libraries + "" + PARENT_SCOPE +) + + +# === Tests === + +if (${include_${PROJECT_NAME}_tests}) +set(TEST_RUNNER "${PROJECT_NAME}_test") +enable_testing() +# TODO(stuartmorgan): Consider using a single shared, pre-checked-in googletest +# instance rather than downloading for each plugin. This approach makes sense +# for a template, but not for a monorepo with many plugins. +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/release-1.11.0.zip +) +# Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# Disable install commands for gtest so it doesn't end up in the bundle. +set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) + +FetchContent_MakeAvailable(googletest) + +# The plugin's C API is not very useful for unit testing, so build the sources +# directly into the test binary rather than using the DLL. +add_executable(${TEST_RUNNER} + test/mocks.h + test/local_auth_plugin_test.cpp + ${PLUGIN_SOURCES} +) +apply_standard_settings(${TEST_RUNNER}) +target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_compile_features(${TEST_RUNNER} PRIVATE cxx_std_20) +target_compile_options(${TEST_RUNNER} PRIVATE /await) +target_link_libraries(${TEST_RUNNER} PRIVATE ${CMAKE_BINARY_DIR}/packages/Microsoft.Windows.ImplementationLibrary.${WIL_VERSION}/build/native/Microsoft.Windows.ImplementationLibrary.targets) +target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin) +target_link_libraries(${TEST_RUNNER} PRIVATE windowsapp) +target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) + +# flutter_wrapper_plugin has link dependencies on the Flutter DLL. +add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${FLUTTER_LIBRARY}" $ +) + +include(GoogleTest) +gtest_discover_tests(${TEST_RUNNER}) +endif() diff --git a/packages/local_auth/local_auth_windows/windows/include/local_auth_windows/local_auth_plugin.h b/packages/local_auth/local_auth_windows/windows/include/local_auth_windows/local_auth_plugin.h new file mode 100644 index 000000000000..0604de8ee2bb --- /dev/null +++ b/packages/local_auth/local_auth_windows/windows/include/local_auth_windows/local_auth_plugin.h @@ -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. +#ifndef FLUTTER_PLUGIN_LOCAL_AUTH_WINDOWS_PLUGIN_H_ +#define FLUTTER_PLUGIN_LOCAL_AUTH_WINDOWS_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void LocalAuthPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_LOCAL_AUTH_WINDOWS_PLUGIN_H_ diff --git a/packages/local_auth/local_auth_windows/windows/local_auth.h b/packages/local_auth/local_auth_windows/windows/local_auth.h new file mode 100644 index 000000000000..94b91f88345a --- /dev/null +++ b/packages/local_auth/local_auth_windows/windows/local_auth.h @@ -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. + +#include +#include +#include +#include +#include +#include +#include + +#include "include/local_auth_windows/local_auth_plugin.h" + +// Include prior to C++/WinRT Headers +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace local_auth_windows { + +// Abstract class that is used to determine whether a user +// has given consent to a particular action, and if the system +// supports asking this question. +class UserConsentVerifier { + public: + UserConsentVerifier() {} + virtual ~UserConsentVerifier() = default; + + // Abstract method that request the user's verification + // given the provided reason. + virtual winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI::UserConsentVerificationResult> + RequestVerificationForWindowAsync(std::wstring localized_reason) = 0; + + // Abstract method that returns weather the system supports Windows Hello. + virtual winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability> + CheckAvailabilityAsync() = 0; + + // Disallow copy and move. + UserConsentVerifier(const UserConsentVerifier&) = delete; + UserConsentVerifier& operator=(const UserConsentVerifier&) = delete; +}; + +class LocalAuthPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); + + // Creates a plugin instance that will create the dialog and associate + // it with the HWND returned from the provided function. + LocalAuthPlugin(std::function window_provider); + + // Creates a plugin instance with the given UserConsentVerifier instance. + // 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(); + + 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); + + // Returns whether the system supports Windows Hello. + winrt::fire_and_forget IsDeviceSupported( + std::unique_ptr> result); +}; + +} // namespace local_auth_windows \ No newline at end of file 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 new file mode 100644 index 000000000000..7a25abb53010 --- /dev/null +++ b/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp @@ -0,0 +1,236 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include + +#include "local_auth.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); +} + +// Converts the given UTF-8 string to UTF-16. +std::wstring Utf16FromUtf8(const std::string& utf8_string) { + if (utf8_string.empty()) { + return std::wstring(); + } + int target_length = + ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8_string.data(), + static_cast(utf8_string.length()), nullptr, 0); + if (target_length == 0) { + return std::wstring(); + } + std::wstring utf16_string; + utf16_string.resize(target_length); + int converted_length = + ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8_string.data(), + static_cast(utf8_string.length()), + utf16_string.data(), target_length); + if (converted_length == 0) { + return std::wstring(); + } + return utf16_string; +} + +} // namespace + +namespace local_auth_windows { + +// Creates an instance of the UserConsentVerifier that +// calls the native Windows APIs to get the user's consent. +class UserConsentVerifierImpl : public UserConsentVerifier { + public: + explicit UserConsentVerifierImpl(std::function window_provider) + : get_root_window_(std::move(window_provider)){}; + virtual ~UserConsentVerifierImpl() = default; + + // Calls the native Windows API to get the user's consent + // with the provided reason. + winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI::UserConsentVerificationResult> + RequestVerificationForWindowAsync(std::wstring localized_reason) override { + winrt::impl::com_ref + user_consent_verifier_interop = winrt::get_activation_factory< + winrt::Windows::Security::Credentials::UI::UserConsentVerifier, + IUserConsentVerifierInterop>(); + + HWND root_window_handle = get_root_window_(); + + auto reason = wil::make_unique_string( + localized_reason.c_str(), localized_reason.size()); + + winrt::Windows::Security::Credentials::UI::UserConsentVerificationResult + consent_result = co_await winrt::capture< + winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI:: + UserConsentVerificationResult>>( + user_consent_verifier_interop, + &::IUserConsentVerifierInterop::RequestVerificationForWindowAsync, + root_window_handle, reason.get()); + + return consent_result; + } + + // Calls the native Windows API to check for the Windows Hello availability. + winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability> + CheckAvailabilityAsync() override { + return winrt::Windows::Security::Credentials::UI::UserConsentVerifier:: + CheckAvailabilityAsync(); + } + + // Disallow copy and move. + UserConsentVerifierImpl(const UserConsentVerifierImpl&) = delete; + UserConsentVerifierImpl& operator=(const UserConsentVerifierImpl&) = delete; + + private: + // The provider for the root window to attach the dialog to. + std::function get_root_window_; +}; + +// 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)); + }); + + registrar->AddPlugin(std::move(plugin)); +} + +// Default constructor for LocalAuthPlugin. +LocalAuthPlugin::LocalAuthPlugin(std::function window_provider) + : user_consent_verifier_(std::make_unique( + std::move(window_provider))) {} + +LocalAuthPlugin::LocalAuthPlugin( + std::unique_ptr user_consent_verifier) + : user_consent_verifier_(std::move(user_consent_verifier)) {} + +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(); + } +} + +// 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::Windows::Security::Credentials::UI::UserConsentVerifierAvailability + ucv_availability = + co_await user_consent_verifier_->CheckAvailabilityAsync(); + + if (ucv_availability == + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::DeviceNotPresent) { + result->Error("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."); + co_return; + } else if (ucv_availability != + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::Available) { + result->Error("NotAvailable", "Required security features not enabled"); + co_return; + } + + try { + winrt::Windows::Security::Credentials::UI::UserConsentVerificationResult + consent_result = + co_await user_consent_verifier_->RequestVerificationForWindowAsync( + reason); + + result->Success(flutter::EncodableValue( + 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()); + } +} + +// Returns whether the device supports Windows Hello or not. +winrt::fire_and_forget LocalAuthPlugin::IsDeviceSupported( + std::unique_ptr> 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)); +} + +} // namespace local_auth_windows diff --git a/packages/local_auth/local_auth_windows/windows/local_auth_windows.cpp b/packages/local_auth/local_auth_windows/windows/local_auth_windows.cpp new file mode 100644 index 000000000000..6e5e6a186afb --- /dev/null +++ b/packages/local_auth/local_auth_windows/windows/local_auth_windows.cpp @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "include/local_auth_windows/local_auth_plugin.h" +#include "local_auth.h" + +void LocalAuthPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + local_auth_windows::LocalAuthPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} 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 new file mode 100644 index 000000000000..3828b05eef07 --- /dev/null +++ b/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp @@ -0,0 +1,253 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "include/local_auth_windows/local_auth_plugin.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "mocks.h" + +namespace local_auth_windows { +namespace test { + +using flutter::EncodableList; +using flutter::EncodableMap; +using flutter::EncodableValue; +using ::testing::_; +using ::testing::DoAll; +using ::testing::EndsWith; +using ::testing::Eq; +using ::testing::Pointee; +using ::testing::Return; + +TEST(LocalAuthPlugin, IsDeviceSupportedHandlerSuccessIfVerifierAvailable) { + 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(EncodableValue(true)))); + + plugin.HandleMethodCall( + flutter::MethodCall("isDeviceSupported", + std::make_unique()), + std::move(result)); +} + +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(); + + 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(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)); +} + +TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenAuthorized) { + 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; + }); + + EXPECT_CALL(*mockConsentVerifier, RequestVerificationForWindowAsync) + .Times(1) + .WillOnce([](std::wstring localizedReason) + -> winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI:: + UserConsentVerificationResult> { + EXPECT_EQ(localizedReason, L"My Reason"); + co_return winrt::Windows::Security::Credentials::UI:: + UserConsentVerificationResult::Verified; + }); + + LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + + 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)); +} + +TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenNotAuthorized) { + 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; + }); + + EXPECT_CALL(*mockConsentVerifier, RequestVerificationForWindowAsync) + .Times(1) + .WillOnce([](std::wstring localizedReason) + -> winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI:: + UserConsentVerificationResult> { + EXPECT_EQ(localizedReason, L"My Reason"); + co_return winrt::Windows::Security::Credentials::UI:: + UserConsentVerificationResult::Canceled; + }); + + LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + + 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)); +} + +} // namespace test +} // namespace local_auth_windows diff --git a/packages/local_auth/local_auth_windows/windows/test/mocks.h b/packages/local_auth/local_auth_windows/windows/test/mocks.h new file mode 100644 index 000000000000..d82ae801b4b9 --- /dev/null +++ b/packages/local_auth/local_auth_windows/windows/test/mocks.h @@ -0,0 +1,63 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef 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 + +#include "../local_auth.h" + +namespace local_auth_windows { +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(){}; + virtual ~MockUserConsentVerifier() = default; + + MOCK_METHOD(winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI:: + UserConsentVerificationResult>, + RequestVerificationForWindowAsync, (std::wstring localizedReason), + (override)); + MOCK_METHOD(winrt::Windows::Foundation::IAsyncOperation< + winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability>, + CheckAvailabilityAsync, (), (override)); + + // Disallow copy and move. + MockUserConsentVerifier(const MockUserConsentVerifier&) = delete; + MockUserConsentVerifier& operator=(const MockUserConsentVerifier&) = delete; +}; + +} // namespace +} // namespace test +} // namespace local_auth_windows + +#endif // PACKAGES_LOCAL_AUTH_LOCAL_AUTH_WINDOWS_WINDOWS_TEST_MOCKS_H_ diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index d71ebf70c796..436523551924 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,21 @@ +## NEXT + +* 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. + +## 2.0.11 + +* Updates references to the obsolete master branch. +* Fixes integration test permission issue on recent versions of macOS. + +## 2.0.10 + +* Removes unnecessary imports. +* Adds OS version support information to README. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.0.9 * Updates documentation on README.md. diff --git a/packages/path_provider/path_provider/README.md b/packages/path_provider/path_provider/README.md index 20d888ff8d73..3a52e3e72050 100644 --- a/packages/path_provider/path_provider/README.md +++ b/packages/path_provider/path_provider/README.md @@ -2,10 +2,14 @@ [![pub package](https://img.shields.io/pub/v/path_provider.svg)](https://pub.dev/packages/path_provider) -A Flutter plugin for finding commonly used locations on the filesystem. +A Flutter plugin for finding commonly used locations on the filesystem. Supports Android, iOS, Linux, macOS and Windows. Not all methods are supported on all platforms. +| | Android | iOS | Linux | macOS | Windows | +|-------------|---------|------|-------|--------|-------------| +| **Support** | SDK 16+ | 9.0+ | Any | 10.11+ | Windows 10+ | + ## Usage To use this plugin, add `path_provider` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). @@ -39,5 +43,4 @@ Directories support by platform: `path_provider` now uses a `PlatformInterface`, meaning that not all platforms share a single `PlatformChannel`-based implementation. With that change, tests should be updated to mock `PathProviderPlatform` rather than `PlatformChannel`. -See this `path_provider` [test](https://github.com/flutter/plugins/blob/master/packages/path_provider/path_provider/test/path_provider_test.dart) for an example. - +See this `path_provider` [test](https://github.com/flutter/plugins/blob/main/packages/path_provider/path_provider/test/path_provider_test.dart) for an example. diff --git a/packages/path_provider/path_provider/example/README.md b/packages/path_provider/path_provider/example/README.md index 1f8ea7189ccd..801f44409938 100644 --- a/packages/path_provider/path_provider/example/README.md +++ b/packages/path_provider/path_provider/example/README.md @@ -1,8 +1,3 @@ # path_provider_example Demonstrates how to use the path_provider plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/path_provider/path_provider/example/android/app/build.gradle b/packages/path_provider/path_provider/example/android/app/build.gradle index 6ef1396ef1ac..6d2bd6dadc36 100644 --- a/packages/path_provider/path_provider/example/android/app/build.gradle +++ b/packages/path_provider/path_provider/example/android/app/build.gradle @@ -58,7 +58,7 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/packages/path_provider/path_provider/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/path_provider/path_provider/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/path_provider/path_provider/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/path_provider/path_provider/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/path_provider/path_provider/example/android/build.gradle b/packages/path_provider/path_provider/example/android/build.gradle index e101ac08df55..c21bff8e0a2f 100644 --- a/packages/path_provider/path_provider/example/android/build.gradle +++ b/packages/path_provider/path_provider/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:7.0.1' } } diff --git a/packages/path_provider/path_provider/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/path_provider/path_provider/example/android/gradle/wrapper/gradle-wrapper.properties index caf54fa2801c..297f2fec363f 100644 --- a/packages/path_provider/path_provider/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/path_provider/path_provider/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip 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 27493a76012c..bf150f66f49b 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 @@ -58,7 +58,7 @@ void main() { } }); - final List _allDirs = [ + final List allDirs = [ null, StorageDirectory.music, StorageDirectory.podcasts, @@ -69,11 +69,11 @@ void main() { StorageDirectory.movies, ]; - for (final StorageDirectory? type in _allDirs) { - test('getExternalStorageDirectories (type: $type)', () async { + for (final StorageDirectory? type in allDirs) { + testWidgets('getExternalStorageDirectories (type: $type)', + (WidgetTester tester) async { if (Platform.isIOS) { - final Future?> result = - getExternalStorageDirectories(type: null); + final Future?> result = getExternalStorageDirectories(); expect(result, throwsA(isInstanceOf())); } else if (Platform.isAndroid) { final List? directories = @@ -92,7 +92,14 @@ void main() { expect(result, throwsA(isInstanceOf())); } else { final Directory? result = await getDownloadsDirectory(); - _verifySampleFile(result, 'downloads'); + 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'); + } } }); } diff --git a/packages/path_provider/path_provider/example/lib/main.dart b/packages/path_provider/path_provider/example/lib/main.dart index 90c2ccb93154..cb9c2eb1798d 100644 --- a/packages/path_provider/path_provider/example/lib/main.dart +++ b/packages/path_provider/path_provider/example/lib/main.dart @@ -10,10 +10,12 @@ import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -31,7 +33,7 @@ class MyHomePage extends StatefulWidget { final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { @@ -138,10 +140,10 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( + onPressed: _requestTempDirectory, child: const Text( 'Get Temporary Directory', ), - onPressed: _requestTempDirectory, ), ), FutureBuilder( @@ -155,10 +157,10 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( + onPressed: _requestAppDocumentsDirectory, child: const Text( 'Get Application Documents Directory', ), - onPressed: _requestAppDocumentsDirectory, ), ), FutureBuilder( @@ -172,10 +174,10 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( + onPressed: _requestAppSupportDirectory, child: const Text( 'Get Application Support Directory', ), - onPressed: _requestAppSupportDirectory, ), ), FutureBuilder( @@ -189,13 +191,13 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( + onPressed: + Platform.isAndroid ? null : _requestAppLibraryDirectory, child: Text( Platform.isAndroid ? 'Application Library Directory unavailable' : 'Get Application Library Directory', ), - onPressed: - Platform.isAndroid ? null : _requestAppLibraryDirectory, ), ), FutureBuilder( @@ -209,14 +211,14 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( + onPressed: !Platform.isAndroid + ? null + : _requestExternalStorageDirectory, child: Text( !Platform.isAndroid ? 'External storage is unavailable' : 'Get External Storage Directory', ), - onPressed: !Platform.isAndroid - ? null - : _requestExternalStorageDirectory, ), ), FutureBuilder( @@ -230,11 +232,6 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( - child: Text( - !Platform.isAndroid - ? 'External directories are unavailable' - : 'Get External Storage Directories', - ), onPressed: !Platform.isAndroid ? null : () { @@ -242,6 +239,11 @@ class _MyHomePageState extends State { StorageDirectory.music, ); }, + child: Text( + !Platform.isAndroid + ? 'External directories are unavailable' + : 'Get External Storage Directories', + ), ), ), FutureBuilder?>( @@ -255,14 +257,14 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( + onPressed: !Platform.isAndroid + ? null + : _requestExternalCacheDirectories, child: Text( !Platform.isAndroid ? 'External directories are unavailable' : 'Get External Cache Directories', ), - onPressed: !Platform.isAndroid - ? null - : _requestExternalCacheDirectories, ), ), FutureBuilder?>( @@ -276,14 +278,14 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( + onPressed: Platform.isAndroid || Platform.isIOS + ? null + : _requestDownloadsDirectory, child: Text( Platform.isAndroid || Platform.isIOS ? 'Downloads directory is unavailable' : 'Get Downloads Directory', ), - onPressed: Platform.isAndroid || Platform.isIOS - ? null - : _requestDownloadsDirectory, ), ), FutureBuilder( diff --git a/packages/path_provider/path_provider/example/linux/flutter/generated_plugins.cmake b/packages/path_provider/path_provider/example/linux/flutter/generated_plugins.cmake index 51436ae8c982..2e1de87a7eb6 100644 --- a/packages/path_provider/path_provider/example/linux/flutter/generated_plugins.cmake +++ b/packages/path_provider/path_provider/example/linux/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -13,3 +16,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/path_provider/path_provider/example/pubspec.yaml b/packages/path_provider/path_provider/example/pubspec.yaml index a279bbade6bf..5964a267f96d 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.5.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -20,6 +20,8 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/path_provider/path_provider/example/windows/flutter/generated_plugins.cmake b/packages/path_provider/path_provider/example/windows/flutter/generated_plugins.cmake index 4d10c2518654..b93c4c30c167 100644 --- a/packages/path_provider/path_provider/example/windows/flutter/generated_plugins.cmake +++ b/packages/path_provider/path_provider/example/windows/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -13,3 +16,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index 30ddfdcfb119..8b68e1264fe7 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.9 +version: 2.0.11 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/path_provider/path_provider/test/path_provider_test.dart b/packages/path_provider/path_provider/test/path_provider_test.dart index 218861606209..aa6d325574df 100644 --- a/packages/path_provider/path_provider/test/path_provider_test.dart +++ b/packages/path_provider/path_provider/test/path_provider_test.dart @@ -8,7 +8,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:test/fake.dart'; const String kTemporaryPath = 'temporaryPath'; const String kApplicationSupportPath = 'applicationSupportPath'; diff --git a/packages/path_provider/path_provider_android/CHANGELOG.md b/packages/path_provider/path_provider_android/CHANGELOG.md index 7b04e90d5e0f..c36a771d3340 100644 --- a/packages/path_provider/path_provider_android/CHANGELOG.md +++ b/packages/path_provider/path_provider_android/CHANGELOG.md @@ -1,3 +1,42 @@ +## NEXT + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. +* Updates minimum Flutter version to 2.10. + +## 2.0.20 + +* Reverts changes in versions 2.0.18 and 2.0.19. + +## 2.0.19 + +* Bumps kotlin to 1.7.10 + +## 2.0.18 + +* Bumps `androidx.annotation:annotation` version to 1.4.0. +* Bumps gradle version to 7.2.2. + +## 2.0.17 + +* Lower minimim version back to 2.8.1. + +## 2.0.16 + +* Fixes bug with `getExternalStoragePaths(null)`. + +## 2.0.15 + +* Switches the medium from MethodChannels to Pigeon. + +## 2.0.14 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 2.0.13 + +* Fixes typing build warning. + ## 2.0.12 * Returns to using a different platform channel name, undoing the revert in diff --git a/packages/path_provider/path_provider_android/android/build.gradle b/packages/path_provider/path_provider_android/android/build.gradle index c7ceec2584c6..32c046bf235f 100644 --- a/packages/path_provider/path_provider_android/android/build.gradle +++ b/packages/path_provider/path_provider_android/android/build.gradle @@ -29,6 +29,8 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { + // TODO(stuartmorgan): Enable when gradle is updated. + // disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' } @@ -54,5 +56,5 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.1.0' implementation 'com.google.guava:guava:28.1-android' - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' } diff --git a/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/Messages.java b/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/Messages.java new file mode 100644 index 000000000000..47144d4a8fcd --- /dev/null +++ b/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/Messages.java @@ -0,0 +1,242 @@ +// 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.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package io.flutter.plugins.pathprovider; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) +public class Messages { + + public enum StorageDirectory { + root(0), + music(1), + podcasts(2), + ringtones(3), + alarms(4), + notifications(5), + pictures(6), + movies(7), + downloads(8), + dcim(9), + documents(10); + + private int index; + + private StorageDirectory(final int index) { + this.index = index; + } + } + + private static class PathProviderApiCodec extends StandardMessageCodec { + public static final PathProviderApiCodec INSTANCE = new PathProviderApiCodec(); + + private PathProviderApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface PathProviderApi { + @Nullable + String getTemporaryPath(); + + @Nullable + String getApplicationSupportPath(); + + @Nullable + String getApplicationDocumentsPath(); + + @Nullable + String getExternalStoragePath(); + + @NonNull + List getExternalCachePaths(); + + @NonNull + List getExternalStoragePaths(@NonNull StorageDirectory directory); + + /** The codec used by PathProviderApi. */ + static MessageCodec getCodec() { + return PathProviderApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `PathProviderApi` to handle messages through the `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, PathProviderApi api) { + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.PathProviderApi.getTemporaryPath", + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + String output = api.getTemporaryPath(); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.PathProviderApi.getApplicationSupportPath", + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + String output = api.getApplicationSupportPath(); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.PathProviderApi.getApplicationDocumentsPath", + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + String output = api.getApplicationDocumentsPath(); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.PathProviderApi.getExternalStoragePath", + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + String output = api.getExternalStoragePath(); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.PathProviderApi.getExternalCachePaths", + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + List output = api.getExternalCachePaths(); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.PathProviderApi.getExternalStoragePaths", + getCodec(), + taskQueue); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + StorageDirectory directoryArg = + args.get(0) == null ? null : StorageDirectory.values()[(int) args.get(0)]; + if (directoryArg == null) { + throw new NullPointerException("directoryArg unexpectedly null."); + } + List output = api.getExternalStoragePaths(directoryArg); + 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()); + errorMap.put("code", exception.getClass().getSimpleName()); + errorMap.put( + "details", + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + return errorMap; + } +} diff --git a/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java b/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java index 278ff58b59dc..7ef82198b22c 100644 --- a/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java +++ b/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java @@ -7,174 +7,34 @@ import android.content.Context; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; -import android.os.Handler; -import android.os.Looper; import android.util.Log; import androidx.annotation.NonNull; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.SettableFuture; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.MethodCodec; -import io.flutter.plugin.common.StandardMethodCodec; +import io.flutter.plugin.common.BinaryMessenger.TaskQueue; +import io.flutter.plugins.pathprovider.Messages.PathProviderApi; import io.flutter.util.PathUtils; import java.io.File; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; +import javax.annotation.Nullable; -public class PathProviderPlugin implements FlutterPlugin, MethodCallHandler { +public class PathProviderPlugin implements FlutterPlugin, PathProviderApi { static final String TAG = "PathProviderPlugin"; private Context context; - private MethodChannel channel; - private PathProviderImpl impl; - - /** - * An abstraction over how to access the paths in a thread-safe manner. - * - *

We need this so on versions of Flutter that support Background Platform Channels this plugin - * can take advantage of it. - * - *

This can be removed after https://github.com/flutter/engine/pull/29147 becomes available on - * the stable branch. - */ - private interface PathProviderImpl { - void getTemporaryDirectory(@NonNull Result result); - - void getApplicationDocumentsDirectory(@NonNull Result result); - - void getStorageDirectory(@NonNull Result result); - - void getExternalCacheDirectories(@NonNull Result result); - - void getExternalStorageDirectories(@NonNull String directoryName, @NonNull Result result); - - void getApplicationSupportDirectory(@NonNull Result result); - } - - /** The implementation for getting system paths that executes from the platform */ - private class PathProviderPlatformThread implements PathProviderImpl { - private final Executor uiThreadExecutor = new UiThreadExecutor(); - private final Executor executor = - Executors.newSingleThreadExecutor( - new ThreadFactoryBuilder() - .setNameFormat("path-provider-background-%d") - .setPriority(Thread.NORM_PRIORITY) - .build()); - - public void getTemporaryDirectory(@NonNull Result result) { - executeInBackground(() -> getPathProviderTemporaryDirectory(), result); - } - - public void getApplicationDocumentsDirectory(@NonNull Result result) { - executeInBackground(() -> getPathProviderApplicationDocumentsDirectory(), result); - } - - public void getStorageDirectory(@NonNull Result result) { - executeInBackground(() -> getPathProviderStorageDirectory(), result); - } - - public void getExternalCacheDirectories(@NonNull Result result) { - executeInBackground(() -> getPathProviderExternalCacheDirectories(), result); - } - - public void getExternalStorageDirectories( - @NonNull String directoryName, @NonNull Result result) { - executeInBackground(() -> getPathProviderExternalStorageDirectories(directoryName), result); - } - - public void getApplicationSupportDirectory(@NonNull Result result) { - executeInBackground(() -> PathProviderPlugin.this.getApplicationSupportDirectory(), result); - } - - private void executeInBackground(Callable task, Result result) { - final SettableFuture future = SettableFuture.create(); - Futures.addCallback( - future, - new FutureCallback() { - public void onSuccess(T answer) { - result.success(answer); - } - - public void onFailure(Throwable t) { - result.error(t.getClass().getName(), t.getMessage(), null); - } - }, - uiThreadExecutor); - executor.execute( - () -> { - try { - future.set(task.call()); - } catch (Throwable t) { - future.setException(t); - } - }); - } - } - - /** The implementation for getting system paths that executes from a background thread. */ - private class PathProviderBackgroundThread implements PathProviderImpl { - public void getTemporaryDirectory(@NonNull Result result) { - result.success(getPathProviderTemporaryDirectory()); - } - - public void getApplicationDocumentsDirectory(@NonNull Result result) { - result.success(getPathProviderApplicationDocumentsDirectory()); - } - - public void getStorageDirectory(@NonNull Result result) { - result.success(getPathProviderStorageDirectory()); - } - - public void getExternalCacheDirectories(@NonNull Result result) { - result.success(getPathProviderExternalCacheDirectories()); - } - - public void getExternalStorageDirectories( - @NonNull String directoryName, @NonNull Result result) { - result.success(getPathProviderExternalStorageDirectories(directoryName)); - } - - public void getApplicationSupportDirectory(@NonNull Result result) { - result.success(PathProviderPlugin.this.getApplicationSupportDirectory()); - } - } public PathProviderPlugin() {} private void setup(BinaryMessenger messenger, Context context) { - String channelName = "plugins.flutter.io/path_provider_android"; - // TODO(gaaclarke): Remove reflection guard when https://github.com/flutter/engine/pull/29147 - // becomes available on the stable branch. + TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + try { - Class methodChannelClass = Class.forName("io.flutter.plugin.common.MethodChannel"); - Class taskQueueClass = Class.forName("io.flutter.plugin.common.BinaryMessenger$TaskQueue"); - Method makeBackgroundTaskQueue = messenger.getClass().getMethod("makeBackgroundTaskQueue"); - Object taskQueue = makeBackgroundTaskQueue.invoke(messenger); - Constructor constructor = - methodChannelClass.getConstructor( - BinaryMessenger.class, String.class, MethodCodec.class, taskQueueClass); - channel = - constructor.newInstance(messenger, channelName, StandardMethodCodec.INSTANCE, taskQueue); - impl = new PathProviderBackgroundThread(); - Log.d(TAG, "Use TaskQueues."); + PathProviderApi.setup(messenger, this); } catch (Exception ex) { - channel = new MethodChannel(messenger, channelName); - impl = new PathProviderPlatformThread(); - Log.d(TAG, "Don't use TaskQueues."); + Log.e(TAG, "Received exception while setting up PathProviderPlugin", ex); } + this.context = context; - channel.setMethodCallHandler(this); } @SuppressWarnings("deprecation") @@ -190,36 +50,38 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - channel.setMethodCallHandler(null); - channel = null; + PathProviderApi.setup(binding.getBinaryMessenger(), null); } @Override - public void onMethodCall(MethodCall call, @NonNull Result result) { - switch (call.method) { - case "getTemporaryDirectory": - impl.getTemporaryDirectory(result); - break; - case "getApplicationDocumentsDirectory": - impl.getApplicationDocumentsDirectory(result); - break; - case "getStorageDirectory": - impl.getStorageDirectory(result); - break; - case "getExternalCacheDirectories": - impl.getExternalCacheDirectories(result); - break; - case "getExternalStorageDirectories": - final Integer type = call.argument("type"); - final String directoryName = StorageDirectoryMapper.androidType(type); - impl.getExternalStorageDirectories(directoryName, result); - break; - case "getApplicationSupportDirectory": - impl.getApplicationSupportDirectory(result); - break; - default: - result.notImplemented(); - } + public @Nullable String getTemporaryPath() { + return getPathProviderTemporaryDirectory(); + } + + @Override + public @Nullable String getApplicationSupportPath() { + return getApplicationSupportDirectory(); + } + + @Override + public @Nullable String getApplicationDocumentsPath() { + return getPathProviderApplicationDocumentsDirectory(); + } + + @Override + public @Nullable String getExternalStoragePath() { + return getPathProviderStorageDirectory(); + } + + @Override + public @NonNull List getExternalCachePaths() { + return getPathProviderExternalCacheDirectories(); + } + + @Override + public @NonNull List getExternalStoragePaths( + @NonNull Messages.StorageDirectory directory) { + return getPathProviderExternalStorageDirectories(directory); } private String getPathProviderTemporaryDirectory() { @@ -261,17 +123,47 @@ private List getPathProviderExternalCacheDirectories() { return paths; } - private List getPathProviderExternalStorageDirectories(String type) { + private String getStorageDirectoryString(@NonNull Messages.StorageDirectory directory) { + switch (directory) { + case root: + return null; + case music: + return "music"; + case podcasts: + return "podcasts"; + case ringtones: + return "ringtones"; + case alarms: + return "alarms"; + case notifications: + return "notifications"; + case pictures: + return "pictures"; + case movies: + return "movies"; + case downloads: + return "downloads"; + case dcim: + return "dcim"; + case documents: + return "documents"; + default: + throw new RuntimeException("Unrecognized directory: " + directory); + } + } + + private List getPathProviderExternalStorageDirectories( + @NonNull Messages.StorageDirectory directory) { final List paths = new ArrayList(); if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { - for (File dir : context.getExternalFilesDirs(type)) { + for (File dir : context.getExternalFilesDirs(getStorageDirectoryString(directory))) { if (dir != null) { paths.add(dir.getAbsolutePath()); } } } else { - File dir = context.getExternalFilesDir(type); + File dir = context.getExternalFilesDir(getStorageDirectoryString(directory)); if (dir != null) { paths.add(dir.getAbsolutePath()); } @@ -279,13 +171,4 @@ private List getPathProviderExternalStorageDirectories(String type) { return paths; } - - private static class UiThreadExecutor implements Executor { - private final Handler handler = new Handler(Looper.getMainLooper()); - - @Override - public void execute(Runnable command) { - handler.post(command); - } - } } diff --git a/packages/path_provider/path_provider_android/example/README.md b/packages/path_provider/path_provider_android/example/README.md index 1f8ea7189ccd..96b8bb17dbff 100644 --- a/packages/path_provider/path_provider_android/example/README.md +++ b/packages/path_provider/path_provider_android/example/README.md @@ -1,8 +1,9 @@ -# path_provider_example +# Platform Implementation Test App -Demonstrates how to use the path_provider plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/path_provider/path_provider_android/example/android/app/build.gradle b/packages/path_provider/path_provider_android/example/android/app/build.gradle index 6ef1396ef1ac..6d2bd6dadc36 100644 --- a/packages/path_provider/path_provider_android/example/android/app/build.gradle +++ b/packages/path_provider/path_provider_android/example/android/app/build.gradle @@ -58,7 +58,7 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/packages/path_provider/path_provider_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/path_provider/path_provider_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/path_provider/path_provider_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/path_provider/path_provider_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/path_provider/path_provider_android/example/android/build.gradle b/packages/path_provider/path_provider_android/example/android/build.gradle index e101ac08df55..c21bff8e0a2f 100644 --- a/packages/path_provider/path_provider_android/example/android/build.gradle +++ b/packages/path_provider/path_provider_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:7.0.1' } } diff --git a/packages/path_provider/path_provider_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/path_provider/path_provider_android/example/android/gradle/wrapper/gradle-wrapper.properties index caf54fa2801c..297f2fec363f 100644 --- a/packages/path_provider/path_provider_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/path_provider/path_provider_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/path_provider/path_provider_android/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_android/example/integration_test/path_provider_test.dart index 426b07abc7c1..ecd0b973343b 100644 --- a/packages/path_provider/path_provider_android/example/integration_test/path_provider_test.dart +++ b/packages/path_provider/path_provider_android/example/integration_test/path_provider_test.dart @@ -49,7 +49,7 @@ void main() { } }); - final List _allDirs = [ + final List allDirs = [ null, StorageDirectory.music, StorageDirectory.podcasts, @@ -60,13 +60,15 @@ void main() { StorageDirectory.movies, ]; - for (final StorageDirectory? type in _allDirs) { - test('getExternalStorageDirectories (type: $type)', () async { + for (final StorageDirectory? type in allDirs) { + testWidgets('getExternalStorageDirectories (type: $type)', + (WidgetTester tester) async { final PathProviderPlatform provider = PathProviderPlatform.instance; final List? directories = await provider.getExternalStoragePaths(type: type); expect(directories, isNotNull); + expect(directories, isNotEmpty); for (final String result in directories!) { _verifySampleFile(result, '$type'); } diff --git a/packages/path_provider/path_provider_android/example/lib/main.dart b/packages/path_provider/path_provider_android/example/lib/main.dart index 6e04f865bfcf..fc9424a33542 100644 --- a/packages/path_provider/path_provider_android/example/lib/main.dart +++ b/packages/path_provider/path_provider_android/example/lib/main.dart @@ -8,10 +8,12 @@ import 'package:flutter/material.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -29,7 +31,7 @@ class MyHomePage extends StatefulWidget { final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { @@ -121,8 +123,8 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( - child: const Text('Get Temporary Directory'), onPressed: _requestTempDirectory, + child: const Text('Get Temporary Directory'), ), ), FutureBuilder( @@ -130,8 +132,8 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( - child: const Text('Get Application Documents Directory'), onPressed: _requestAppDocumentsDirectory, + child: const Text('Get Application Documents Directory'), ), ), FutureBuilder( @@ -139,8 +141,8 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( - child: const Text('Get Application Support Directory'), onPressed: _requestAppSupportDirectory, + child: const Text('Get Application Support Directory'), ), ), FutureBuilder( @@ -148,8 +150,8 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( - child: const Text('Get External Storage Directory'), onPressed: _requestExternalStorageDirectory, + child: const Text('Get External Storage Directory'), ), ), FutureBuilder( @@ -174,8 +176,8 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( - child: const Text('Get External Cache Directories'), onPressed: _requestExternalCacheDirectories, + child: const Text('Get External Cache Directories'), ), ), ]), diff --git a/packages/path_provider/path_provider_android/example/pubspec.yaml b/packages/path_provider/path_provider_android/example/pubspec.yaml index 75617d8f9747..b460d6ba49ce 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.5.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -21,6 +21,8 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/path_provider/path_provider_android/lib/messages.g.dart b/packages/path_provider/path_provider_android/lib/messages.g.dart new file mode 100644 index 000000000000..cf095c244b8d --- /dev/null +++ b/packages/path_provider/path_provider_android/lib/messages.g.dart @@ -0,0 +1,197 @@ +// 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.0), 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 +// @dart = 2.12 +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'; + +enum StorageDirectory { + root, + music, + podcasts, + ringtones, + alarms, + notifications, + pictures, + movies, + downloads, + dcim, + documents, +} + +class _PathProviderApiCodec extends StandardMessageCodec { + const _PathProviderApiCodec(); +} + +class PathProviderApi { + /// Constructor for [PathProviderApi]. 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. + PathProviderApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _PathProviderApiCodec(); + + Future getTemporaryPath() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getTemporaryPath', 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 (replyMap['result'] as String?); + } + } + + Future getApplicationSupportPath() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getApplicationSupportPath', 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 (replyMap['result'] as String?); + } + } + + Future getApplicationDocumentsPath() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getApplicationDocumentsPath', 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 (replyMap['result'] as String?); + } + } + + Future getExternalStoragePath() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getExternalStoragePath', 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 (replyMap['result'] as String?); + } + } + + Future> getExternalCachePaths() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getExternalCachePaths', 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 List?)!.cast(); + } + } + + Future> getExternalStoragePaths( + StorageDirectory arg_directory) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getExternalStoragePaths', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_directory.index]) 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(); + } + } +} diff --git a/packages/path_provider/path_provider_android/lib/path_provider_android.dart b/packages/path_provider/path_provider_android/lib/path_provider_android.dart index b0f3808d2859..f5c74f540253 100644 --- a/packages/path_provider/path_provider_android/lib/path_provider_android.dart +++ b/packages/path_provider/path_provider_android/lib/path_provider_android.dart @@ -2,16 +2,40 @@ // 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/services.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'messages.g.dart' as messages; + +messages.StorageDirectory _convertStorageDirectory( + StorageDirectory? directory) { + switch (directory) { + case null: + return messages.StorageDirectory.root; + case StorageDirectory.music: + return messages.StorageDirectory.music; + case StorageDirectory.podcasts: + return messages.StorageDirectory.podcasts; + case StorageDirectory.ringtones: + return messages.StorageDirectory.ringtones; + case StorageDirectory.alarms: + return messages.StorageDirectory.alarms; + case StorageDirectory.notifications: + return messages.StorageDirectory.notifications; + case StorageDirectory.pictures: + return messages.StorageDirectory.pictures; + case StorageDirectory.movies: + return messages.StorageDirectory.movies; + case StorageDirectory.downloads: + return messages.StorageDirectory.downloads; + case StorageDirectory.dcim: + return messages.StorageDirectory.dcim; + case StorageDirectory.documents: + return messages.StorageDirectory.documents; + } +} /// The Android implementation of [PathProviderPlatform]. class PathProviderAndroid extends PathProviderPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - MethodChannel methodChannel = - const MethodChannel('plugins.flutter.io/path_provider_android'); + final messages.PathProviderApi _api = messages.PathProviderApi(); /// Registers this class as the default instance of [PathProviderPlatform]. static void registerWith() { @@ -20,12 +44,12 @@ class PathProviderAndroid extends PathProviderPlatform { @override Future getTemporaryPath() { - return methodChannel.invokeMethod('getTemporaryDirectory'); + return _api.getTemporaryPath(); } @override Future getApplicationSupportPath() { - return methodChannel.invokeMethod('getApplicationSupportDirectory'); + return _api.getApplicationSupportPath(); } @override @@ -35,29 +59,25 @@ class PathProviderAndroid extends PathProviderPlatform { @override Future getApplicationDocumentsPath() { - return methodChannel - .invokeMethod('getApplicationDocumentsDirectory'); + return _api.getApplicationDocumentsPath(); } @override Future getExternalStoragePath() { - return methodChannel.invokeMethod('getStorageDirectory'); + return _api.getExternalStoragePath(); } @override - Future?> getExternalCachePaths() { - return methodChannel - .invokeListMethod('getExternalCacheDirectories'); + Future?> getExternalCachePaths() async { + return (await _api.getExternalCachePaths()).cast(); } @override Future?> getExternalStoragePaths({ StorageDirectory? type, }) async { - return methodChannel.invokeListMethod( - 'getExternalStorageDirectories', - {'type': type?.index}, - ); + return (await _api.getExternalStoragePaths(_convertStorageDirectory(type))) + .cast(); } @override diff --git a/packages/path_provider/path_provider_android/pigeons/copyright.txt b/packages/path_provider/path_provider_android/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/path_provider/path_provider_android/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/path_provider/path_provider_android/pigeons/messages.dart b/packages/path_provider/path_provider_android/pigeons/messages.dart new file mode 100644 index 000000000000..96ad6343d3b0 --- /dev/null +++ b/packages/path_provider/path_provider_android/pigeons/messages.dart @@ -0,0 +1,44 @@ +// 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( + input: 'pigeons/messages.dart', + javaOut: + 'android/src/main/java/io/flutter/plugins/pathprovider/Messages.java', + javaOptions: JavaOptions( + className: 'Messages', package: 'io.flutter.plugins.pathprovider'), + dartOut: 'lib/messages.g.dart', + dartTestOut: 'test/messages_test.g.dart', + copyrightHeader: 'pigeons/copyright.txt', +)) +enum StorageDirectory { + root, + music, + podcasts, + ringtones, + alarms, + notifications, + pictures, + movies, + downloads, + dcim, + documents, +} + +@HostApi(dartHostTestHandler: 'TestPathProviderApi') +abstract class PathProviderApi { + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + String? getTemporaryPath(); + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + String? getApplicationSupportPath(); + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + String? getApplicationDocumentsPath(); + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + String? getExternalStoragePath(); + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getExternalCachePaths(); + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getExternalStoragePaths(StorageDirectory directory); +} diff --git a/packages/path_provider/path_provider_android/pubspec.yaml b/packages/path_provider/path_provider_android/pubspec.yaml index 63b9330a89f9..fba4c32506fe 100644 --- a/packages/path_provider/path_provider_android/pubspec.yaml +++ b/packages/path_provider/path_provider_android/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider_android description: Android implementation of the path_provider plugin. repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.12 +version: 2.0.20 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.0" flutter: plugin: @@ -29,4 +29,5 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter + pigeon: ^3.1.5 test: ^1.16.0 diff --git a/packages/path_provider/path_provider_android/test/messages_test.g.dart b/packages/path_provider/path_provider_android/test/messages_test.g.dart new file mode 100644 index 000000000000..dc8ee55acc3b --- /dev/null +++ b/packages/path_provider/path_provider_android/test/messages_test.g.dart @@ -0,0 +1,131 @@ +// 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.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis +// ignore_for_file: avoid_relative_lib_imports +// @dart = 2.12 +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'; + +// The following line is edited by hand to avoid confusing dart with overloaded types. +import 'package:path_provider_android/messages.g.dart'; + +class _TestPathProviderApiCodec extends StandardMessageCodec { + const _TestPathProviderApiCodec(); +} + +abstract class TestPathProviderApi { + static const MessageCodec codec = _TestPathProviderApiCodec(); + + String? getTemporaryPath(); + String? getApplicationSupportPath(); + String? getApplicationDocumentsPath(); + String? getExternalStoragePath(); + List getExternalCachePaths(); + List getExternalStoragePaths(StorageDirectory directory); + static void setup(TestPathProviderApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getTemporaryPath', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final String? output = api.getTemporaryPath(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getApplicationSupportPath', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final String? output = api.getApplicationSupportPath(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getApplicationDocumentsPath', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final String? output = api.getApplicationDocumentsPath(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getExternalStoragePath', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final String? output = api.getExternalStoragePath(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getExternalCachePaths', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final List output = api.getExternalCachePaths(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getExternalStoragePaths', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PathProviderApi.getExternalStoragePaths was null.'); + final List args = (message as List?)!; + + /// TODO(gaaclarke): The following line was tweaked by hand to address + /// https://github.com/flutter/flutter/issues/105742. Alternatively + /// the tests could be written with a mock BinaryMessenger but this is + /// how we want to address it eventually. + final StorageDirectory? arg_directory = + StorageDirectory.values[args[0] as int]; + assert(arg_directory != null, + 'Argument for dev.flutter.pigeon.PathProviderApi.getExternalStoragePaths was null, expected non-null StorageDirectory.'); + final List output = + api.getExternalStoragePaths(arg_directory!); + return {'result': output}; + }); + } + } + } +} diff --git a/packages/path_provider/path_provider_android/test/path_provider_android_test.dart b/packages/path_provider/path_provider_android/test/path_provider_android_test.dart index d2f9682bf6d7..e3011474a2a3 100644 --- a/packages/path_provider/path_provider_android/test/path_provider_android_test.dart +++ b/packages/path_provider/path_provider_android/test/path_provider_android_test.dart @@ -2,73 +2,59 @@ // 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:path_provider_android/messages.g.dart' as messages; import 'package:path_provider_android/path_provider_android.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'messages_test.g.dart'; + +const String kTemporaryPath = 'temporaryPath'; +const String kApplicationSupportPath = 'applicationSupportPath'; +const String kLibraryPath = 'libraryPath'; +const String kApplicationDocumentsPath = 'applicationDocumentsPath'; +const String kExternalCachePaths = 'externalCachePaths'; +const String kExternalStoragePaths = 'externalStoragePaths'; +const String kDownloadsPath = 'downloadsPath'; + +class _Api implements TestPathProviderApi { + @override + String? getApplicationDocumentsPath() => kApplicationDocumentsPath; + + @override + String? getApplicationSupportPath() => kApplicationSupportPath; + + @override + List getExternalCachePaths() => [kExternalCachePaths]; + + @override + String? getExternalStoragePath() => kExternalStoragePaths; + + @override + List getExternalStoragePaths(messages.StorageDirectory directory) => + [kExternalStoragePaths]; + + @override + String? getTemporaryPath() => kTemporaryPath; +} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - const String kTemporaryPath = 'temporaryPath'; - const String kApplicationSupportPath = 'applicationSupportPath'; - const String kLibraryPath = 'libraryPath'; - const String kApplicationDocumentsPath = 'applicationDocumentsPath'; - const String kExternalCachePaths = 'externalCachePaths'; - const String kExternalStoragePaths = 'externalStoragePaths'; - const String kDownloadsPath = 'downloadsPath'; group('PathProviderAndroid', () { late PathProviderAndroid pathProvider; - final List log = []; setUp(() async { pathProvider = PathProviderAndroid(); - TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger - .setMockMethodCallHandler(pathProvider.methodChannel, - (MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'getTemporaryDirectory': - return kTemporaryPath; - case 'getApplicationSupportDirectory': - return kApplicationSupportPath; - case 'getLibraryDirectory': - return kLibraryPath; - case 'getApplicationDocumentsDirectory': - return kApplicationDocumentsPath; - case 'getExternalStorageDirectories': - return [kExternalStoragePaths]; - case 'getExternalCacheDirectories': - return [kExternalCachePaths]; - case 'getDownloadsDirectory': - return kDownloadsPath; - default: - return null; - } - }); - }); - - tearDown(() { - log.clear(); + TestPathProviderApi.setup(_Api()); }); test('getTemporaryPath', () async { final String? path = await pathProvider.getTemporaryPath(); - expect( - log, - [isMethodCall('getTemporaryDirectory', arguments: null)], - ); expect(path, kTemporaryPath); }); test('getApplicationSupportPath', () async { final String? path = await pathProvider.getApplicationSupportPath(); - expect( - log, - [ - isMethodCall('getApplicationSupportDirectory', arguments: null) - ], - ); expect(path, kApplicationSupportPath); }); @@ -83,42 +69,21 @@ void main() { test('getApplicationDocumentsPath', () async { final String? path = await pathProvider.getApplicationDocumentsPath(); - expect( - log, - [ - isMethodCall('getApplicationDocumentsDirectory', arguments: null) - ], - ); expect(path, kApplicationDocumentsPath); }); test('getExternalCachePaths succeeds', () async { final List? result = await pathProvider.getExternalCachePaths(); - expect( - log, - [isMethodCall('getExternalCacheDirectories', arguments: null)], - ); expect(result!.length, 1); expect(result.first, kExternalCachePaths); }); for (final StorageDirectory? type in [ - null, ...StorageDirectory.values ]) { test('getExternalStoragePaths (type: $type) android succeeds', () async { final List? result = await pathProvider.getExternalStoragePaths(type: type); - expect( - log, - [ - isMethodCall( - 'getExternalStorageDirectories', - arguments: {'type': type?.index}, - ) - ], - ); - expect(result!.length, 1); expect(result.first, kExternalStoragePaths); }); diff --git a/packages/path_provider/path_provider_ios/CHANGELOG.md b/packages/path_provider/path_provider_ios/CHANGELOG.md index 543af778d2e2..ffc158c19156 100644 --- a/packages/path_provider/path_provider_ios/CHANGELOG.md +++ b/packages/path_provider/path_provider_ios/CHANGELOG.md @@ -1,3 +1,20 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 2.0.11 + +* Lower minimim version back to 2.8. + +## 2.0.10 + +* Switches backend to pigeon. + +## 2.0.9 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.0.8 * Switches to a package-internal implementation of the platform interface. diff --git a/packages/path_provider/path_provider_ios/example/README.md b/packages/path_provider/path_provider_ios/example/README.md index 1f8ea7189ccd..96b8bb17dbff 100644 --- a/packages/path_provider/path_provider_ios/example/README.md +++ b/packages/path_provider/path_provider_ios/example/README.md @@ -1,8 +1,9 @@ -# path_provider_example +# Platform Implementation Test App -Demonstrates how to use the path_provider plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.pbxproj index fec246b9e08c..601985b46ae6 100644 --- a/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ @@ -512,6 +512,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -533,6 +534,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/path_provider/path_provider_ios/example/lib/main.dart b/packages/path_provider/path_provider_ios/example/lib/main.dart index 8c8d5410f923..d7140b76a06b 100644 --- a/packages/path_provider/path_provider_ios/example/lib/main.dart +++ b/packages/path_provider/path_provider_ios/example/lib/main.dart @@ -8,10 +8,12 @@ import 'package:flutter/material.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -29,7 +31,7 @@ class MyHomePage extends StatefulWidget { final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { @@ -90,8 +92,8 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( - child: const Text('Get Temporary Directory'), onPressed: _requestTempDirectory, + child: const Text('Get Temporary Directory'), ), ), FutureBuilder( @@ -99,8 +101,8 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( - child: const Text('Get Application Documents Directory'), onPressed: _requestAppDocumentsDirectory, + child: const Text('Get Application Documents Directory'), ), ), FutureBuilder( @@ -108,8 +110,8 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( - child: const Text('Get Application Support Directory'), onPressed: _requestAppSupportDirectory, + child: const Text('Get Application Support Directory'), ), ), FutureBuilder( @@ -117,8 +119,8 @@ class _MyHomePageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( - child: const Text('Get Application Library Directory'), onPressed: _requestAppLibraryDirectory, + child: const Text('Get Application Library Directory'), ), ), FutureBuilder( diff --git a/packages/path_provider/path_provider_ios/example/pubspec.yaml b/packages/path_provider/path_provider_ios/example/pubspec.yaml index 2166076db2b9..f1d885513948 100644 --- a/packages/path_provider/path_provider_ios/example/pubspec.yaml +++ b/packages/path_provider/path_provider_ios/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -21,6 +21,8 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.m b/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.m index ac6a1be9414f..82b8df5382fb 100644 --- a/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.m +++ b/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.m @@ -3,47 +3,41 @@ // found in the LICENSE file. #import "FLTPathProviderPlugin.h" +#import "messages.g.h" -NSString *GetDirectoryOfType(NSSearchPathDirectory dir) { +static NSString *GetDirectoryOfType(NSSearchPathDirectory dir) { NSArray *paths = NSSearchPathForDirectoriesInDomains(dir, NSUserDomainMask, YES); return paths.firstObject; } +@interface FLTPathProviderPlugin () +@end + @implementation FLTPathProviderPlugin + (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/path_provider_ios" - binaryMessenger:registrar.messenger]; - [channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { - if ([@"getTemporaryDirectory" isEqualToString:call.method]) { - result([self getTemporaryDirectory]); - } else if ([@"getApplicationDocumentsDirectory" isEqualToString:call.method]) { - result([self getApplicationDocumentsDirectory]); - } else if ([@"getApplicationSupportDirectory" isEqualToString:call.method]) { - result([self getApplicationSupportDirectory]); - } else if ([@"getLibraryDirectory" isEqualToString:call.method]) { - result([self getLibraryDirectory]); - } else { - result(FlutterMethodNotImplemented); - } - }]; -} - -+ (NSString *)getTemporaryDirectory { - return GetDirectoryOfType(NSCachesDirectory); + FLTPathProviderPlugin *plugin = [[FLTPathProviderPlugin alloc] init]; + FLTPathProviderApiSetup(registrar.messenger, plugin); } -+ (NSString *)getApplicationDocumentsDirectory { +- (nullable NSString *)getApplicationDocumentsPathWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { return GetDirectoryOfType(NSDocumentDirectory); } -+ (NSString *)getApplicationSupportDirectory { +- (nullable NSString *)getApplicationSupportPathWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { return GetDirectoryOfType(NSApplicationSupportDirectory); } -+ (NSString *)getLibraryDirectory { +- (nullable NSString *)getLibraryPathWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { return GetDirectoryOfType(NSLibraryDirectory); } +- (nullable NSString *)getTemporaryPathWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return GetDirectoryOfType(NSCachesDirectory); +} + @end diff --git a/packages/path_provider/path_provider_ios/ios/Classes/messages.g.h b/packages/path_provider/path_provider_ios/ios/Classes/messages.g.h new file mode 100644 index 000000000000..b6c1d4d92dd4 --- /dev/null +++ b/packages/path_provider/path_provider_ios/ios/Classes/messages.g.h @@ -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. +// Autogenerated from Pigeon (v3.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +/// The codec used by FLTPathProviderApi. +NSObject *FLTPathProviderApiGetCodec(void); + +@protocol FLTPathProviderApi +- (nullable NSString *)getTemporaryPathWithError:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSString *)getApplicationSupportPathWithError:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSString *)getLibraryPathWithError:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSString *)getApplicationDocumentsPathWithError: + (FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FLTPathProviderApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +NS_ASSUME_NONNULL_END diff --git a/packages/path_provider/path_provider_ios/ios/Classes/messages.g.m b/packages/path_provider/path_provider_ios/ios/Classes/messages.g.m new file mode 100644 index 000000000000..2589df1837e7 --- /dev/null +++ b/packages/path_provider/path_provider_ios/ios/Classes/messages.g.m @@ -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. +// Autogenerated from Pigeon (v3.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import "messages.g.h" +#import + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSDictionary *wrapResult(id result, FlutterError *error) { + NSDictionary *errorDict = (NSDictionary *)[NSNull null]; + if (error) { + errorDict = @{ + @"code" : (error.code ?: [NSNull null]), + @"message" : (error.message ?: [NSNull null]), + @"details" : (error.details ?: [NSNull null]), + }; + } + return @{ + @"result" : (result ?: [NSNull null]), + @"error" : errorDict, + }; +} + +@interface FLTPathProviderApiCodecReader : FlutterStandardReader +@end +@implementation FLTPathProviderApiCodecReader +@end + +@interface FLTPathProviderApiCodecWriter : FlutterStandardWriter +@end +@implementation FLTPathProviderApiCodecWriter +@end + +@interface FLTPathProviderApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FLTPathProviderApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FLTPathProviderApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FLTPathProviderApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FLTPathProviderApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FLTPathProviderApiCodecReaderWriter *readerWriter = + [[FLTPathProviderApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FLTPathProviderApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.PathProviderApi.getTemporaryPath" + binaryMessenger:binaryMessenger + codec:FLTPathProviderApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(getTemporaryPathWithError:)], + @"FLTPathProviderApi api (%@) doesn't respond to @selector(getTemporaryPathWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSString *output = [api getTemporaryPathWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.PathProviderApi.getApplicationSupportPath" + binaryMessenger:binaryMessenger + codec:FLTPathProviderApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getApplicationSupportPathWithError:)], + @"FLTPathProviderApi api (%@) doesn't respond to " + @"@selector(getApplicationSupportPathWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSString *output = [api getApplicationSupportPathWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.PathProviderApi.getLibraryPath" + binaryMessenger:binaryMessenger + codec:FLTPathProviderApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(getLibraryPathWithError:)], + @"FLTPathProviderApi api (%@) doesn't respond to @selector(getLibraryPathWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSString *output = [api getLibraryPathWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.PathProviderApi.getApplicationDocumentsPath" + binaryMessenger:binaryMessenger + codec:FLTPathProviderApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getApplicationDocumentsPathWithError:)], + @"FLTPathProviderApi api (%@) doesn't respond to " + @"@selector(getApplicationDocumentsPathWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSString *output = [api getApplicationDocumentsPathWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/path_provider/path_provider_ios/lib/messages.g.dart b/packages/path_provider/path_provider_ios/lib/messages.g.dart new file mode 100644 index 000000000000..1914119b8bd8 --- /dev/null +++ b/packages/path_provider/path_provider_ios/lib/messages.g.dart @@ -0,0 +1,124 @@ +// 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.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name +// @dart = 2.12 +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 _PathProviderApiCodec extends StandardMessageCodec { + const _PathProviderApiCodec(); +} + +class PathProviderApi { + /// Constructor for [PathProviderApi]. 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. + PathProviderApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _PathProviderApiCodec(); + + Future getTemporaryPath() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getTemporaryPath', 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 (replyMap['result'] as String?); + } + } + + Future getApplicationSupportPath() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getApplicationSupportPath', 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 (replyMap['result'] as String?); + } + } + + Future getLibraryPath() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getLibraryPath', 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 (replyMap['result'] as String?); + } + } + + Future getApplicationDocumentsPath() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getApplicationDocumentsPath', 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 (replyMap['result'] as String?); + } + } +} diff --git a/packages/path_provider/path_provider_ios/lib/path_provider_ios.dart b/packages/path_provider/path_provider_ios/lib/path_provider_ios.dart index 88becf20850e..05be0534764a 100644 --- a/packages/path_provider/path_provider_ios/lib/path_provider_ios.dart +++ b/packages/path_provider/path_provider_ios/lib/path_provider_ios.dart @@ -4,16 +4,13 @@ import 'dart:io'; -import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'messages.g.dart'; /// The iOS implementation of [PathProviderPlatform]. class PathProviderIOS extends PathProviderPlatform { /// The method channel used to interact with the native platform. - @visibleForTesting - MethodChannel methodChannel = - const MethodChannel('plugins.flutter.io/path_provider_ios'); + final PathProviderApi _pathProvider = PathProviderApi(); /// Registers this class as the default instance of [PathProviderPlatform] static void registerWith() { @@ -22,13 +19,12 @@ class PathProviderIOS extends PathProviderPlatform { @override Future getTemporaryPath() async { - return methodChannel.invokeMethod('getTemporaryDirectory'); + return _pathProvider.getTemporaryPath(); } @override Future getApplicationSupportPath() async { - final String? path = await methodChannel - .invokeMethod('getApplicationSupportDirectory'); + final String? path = await _pathProvider.getApplicationSupportPath(); if (path != null) { // Ensure the directory exists before returning it, for consistency with // other platforms. @@ -39,13 +35,12 @@ class PathProviderIOS extends PathProviderPlatform { @override Future getLibraryPath() async { - return methodChannel.invokeMethod('getLibraryDirectory'); + return _pathProvider.getLibraryPath(); } @override Future getApplicationDocumentsPath() async { - return methodChannel - .invokeMethod('getApplicationDocumentsDirectory'); + return _pathProvider.getApplicationDocumentsPath(); } @override diff --git a/packages/path_provider/path_provider_ios/pigeons/copyright.txt b/packages/path_provider/path_provider_ios/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/path_provider/path_provider_ios/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/path_provider/path_provider_ios/pigeons/messages.dart b/packages/path_provider/path_provider_ios/pigeons/messages.dart new file mode 100644 index 000000000000..2ed79914e821 --- /dev/null +++ b/packages/path_provider/path_provider_ios/pigeons/messages.dart @@ -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 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + input: 'pigeons/messages.dart', + objcOptions: ObjcOptions(prefix: 'FLT'), + objcHeaderOut: 'ios/Classes/messages.g.h', + objcSourceOut: 'ios/Classes/messages.g.m', + dartOut: 'lib/messages.g.dart', + dartTestOut: 'test/messages_test.g.dart', + copyrightHeader: 'pigeons/copyright.txt', +)) +@HostApi(dartHostTestHandler: 'TestPathProviderApi') +abstract class PathProviderApi { + String? getTemporaryPath(); + String? getApplicationSupportPath(); + String? getLibraryPath(); + String? getApplicationDocumentsPath(); +} diff --git a/packages/path_provider/path_provider_ios/pubspec.yaml b/packages/path_provider/path_provider_ios/pubspec.yaml index 282f8e4f0ebd..2f6171cd70bd 100644 --- a/packages/path_provider/path_provider_ios/pubspec.yaml +++ b/packages/path_provider/path_provider_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider_ios description: iOS implementation of the path_provider plugin. repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.8 +version: 2.0.11 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.0" flutter: plugin: @@ -29,5 +29,6 @@ dev_dependencies: integration_test: sdk: flutter path: ^1.8.0 + pigeon: ^3.1.5 plugin_platform_interface: ^2.0.0 test: ^1.16.0 diff --git a/packages/path_provider/path_provider_ios/test/messages_test.g.dart b/packages/path_provider/path_provider_ios/test/messages_test.g.dart new file mode 100644 index 000000000000..d1c9ff88dca1 --- /dev/null +++ b/packages/path_provider/path_provider_ios/test/messages_test.g.dart @@ -0,0 +1,88 @@ +// 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.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis +// ignore_for_file: avoid_relative_lib_imports +// @dart = 2.12 +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 '../lib/messages.g.dart'; + +class _TestPathProviderApiCodec extends StandardMessageCodec { + const _TestPathProviderApiCodec(); +} + +abstract class TestPathProviderApi { + static const MessageCodec codec = _TestPathProviderApiCodec(); + + String? getTemporaryPath(); + String? getApplicationSupportPath(); + String? getLibraryPath(); + String? getApplicationDocumentsPath(); + static void setup(TestPathProviderApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getTemporaryPath', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final String? output = api.getTemporaryPath(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getApplicationSupportPath', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final String? output = api.getApplicationSupportPath(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getLibraryPath', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final String? output = api.getLibraryPath(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getApplicationDocumentsPath', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final String? output = api.getApplicationDocumentsPath(); + return {'result': output}; + }); + } + } + } +} diff --git a/packages/path_provider/path_provider_ios/test/path_provider_ios_test.dart b/packages/path_provider/path_provider_ios/test/path_provider_ios_test.dart index 40f81c5f445a..16a7cd8d71a2 100644 --- a/packages/path_provider/path_provider_ios/test/path_provider_ios_test.dart +++ b/packages/path_provider/path_provider_ios/test/path_provider_ios_test.dart @@ -4,17 +4,35 @@ import 'dart:io'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; import 'package:path_provider_ios/path_provider_ios.dart'; +import 'messages_test.g.dart'; + +class _Api implements TestPathProviderApi { + String? applicationDocumentsPath; + String? applicationSupportPath; + String? libraryPath; + String? temporaryPath; + + @override + String? getApplicationDocumentsPath() => applicationDocumentsPath; + + @override + String? getApplicationSupportPath() => applicationSupportPath; + + @override + String? getLibraryPath() => libraryPath; + + @override + String? getTemporaryPath() => temporaryPath; +} void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('PathProviderIOS', () { late PathProviderIOS pathProvider; - late List log; // These unit tests use the actual filesystem, since an injectable // filesystem would add a runtime dependency to the package, so everything // is contained to a temporary directory. @@ -24,6 +42,7 @@ void main() { late String applicationSupportPath; late String libraryPath; late String applicationDocumentsPath; + late _Api api; setUp(() async { pathProvider = PathProviderIOS(); @@ -37,24 +56,12 @@ void main() { applicationDocumentsPath = p.join(basePath, 'application', 'documents', 'path'); - log = []; - TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger - .setMockMethodCallHandler(pathProvider.methodChannel, - (MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'getTemporaryDirectory': - return temporaryPath; - case 'getApplicationSupportDirectory': - return applicationSupportPath; - case 'getLibraryDirectory': - return libraryPath; - case 'getApplicationDocumentsDirectory': - return applicationDocumentsPath; - default: - return null; - } - }); + api = _Api(); + api.applicationDocumentsPath = applicationDocumentsPath; + api.applicationSupportPath = applicationSupportPath; + api.libraryPath = libraryPath; + api.temporaryPath = temporaryPath; + TestPathProviderApi.setup(api); }); tearDown(() { @@ -63,21 +70,11 @@ void main() { test('getTemporaryPath', () async { final String? path = await pathProvider.getTemporaryPath(); - expect( - log, - [isMethodCall('getTemporaryDirectory', arguments: null)], - ); expect(path, temporaryPath); }); test('getApplicationSupportPath', () async { final String? path = await pathProvider.getApplicationSupportPath(); - expect( - log, - [ - isMethodCall('getApplicationSupportDirectory', arguments: null) - ], - ); expect(path, applicationSupportPath); }); @@ -89,21 +86,11 @@ void main() { test('getLibraryPath', () async { final String? path = await pathProvider.getLibraryPath(); - expect( - log, - [isMethodCall('getLibraryDirectory', arguments: null)], - ); expect(path, libraryPath); }); test('getApplicationDocumentsPath', () async { final String? path = await pathProvider.getApplicationDocumentsPath(); - expect( - log, - [ - isMethodCall('getApplicationDocumentsDirectory', arguments: null) - ], - ); expect(path, applicationDocumentsPath); }); diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index 55235c3542f9..baf3283348de 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,3 +1,16 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 2.1.7 + +* Bumps ffi dependency to match path_provider_windows. + +## 2.1.6 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.1.5 * Removes dependency on `meta`. @@ -49,20 +62,24 @@ * Check in linux/ directory for example/ -## 0.1.1 - NOT PUBLISHED +## 0.1.1 - NOT PUBLISHED + * Reverts changes on 0.1.0, which broke the tree. +## 0.1.0 - NOT PUBLISHED -## 0.1.0 - NOT PUBLISHED * This release updates getApplicationSupportPath to use the application ID instead of the executable name. * No migration is provided, so any older apps that were using this path will now have a different directory. ## 0.0.1+2 + * This release updates the example to depend on the endorsed plugin rather than relative path ## 0.0.1+1 + * This updates the readme and pubspec and example to reflect the endorsement of this implementation of `path_provider` ## 0.0.1 + * The initial implementation of path\_provider for Linux * Implements getApplicationSupportPath, getApplicationDocumentsPath, getDownloadsPath, and getTemporaryPath diff --git a/packages/path_provider/path_provider_linux/example/README.md b/packages/path_provider/path_provider_linux/example/README.md index 751fe4b811f0..96b8bb17dbff 100644 --- a/packages/path_provider/path_provider_linux/example/README.md +++ b/packages/path_provider/path_provider_linux/example/README.md @@ -1,16 +1,9 @@ -# path_provider_linux_example +# Platform Implementation Test App -Demonstrates how to use the path_provider_linux plugin. +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. -## 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://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/path_provider/path_provider_linux/example/lib/main.dart b/packages/path_provider/path_provider_linux/example/lib/main.dart index d365e6bdeab4..1c7c7e87397a 100644 --- a/packages/path_provider/path_provider_linux/example/lib/main.dart +++ b/packages/path_provider/path_provider_linux/example/lib/main.dart @@ -7,13 +7,16 @@ import 'package:flutter/services.dart'; import 'package:path_provider_linux/path_provider_linux.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } /// Sample app class MyApp extends StatefulWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { diff --git a/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugins.cmake b/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugins.cmake index 51436ae8c982..2e1de87a7eb6 100644 --- a/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugins.cmake +++ b/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -13,3 +16,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/path_provider/path_provider_linux/example/pubspec.yaml b/packages/path_provider/path_provider_linux/example/pubspec.yaml index 252f3510a789..8d8940ba2f05 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: ">=1.20.0" + flutter: ">=2.10.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index 91304fc0b268..41d587360b5e 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.5 +version: 2.1.7 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: @@ -16,7 +16,7 @@ flutter: dartPluginClass: PathProviderLinux dependencies: - ffi: ^1.1.2 + ffi: ">=1.1.2 <3.0.0" flutter: sdk: flutter path: ^1.8.0 diff --git a/packages/path_provider/path_provider_macos/CHANGELOG.md b/packages/path_provider/path_provider_macos/CHANGELOG.md index 047792f8bcc4..61727e4d364b 100644 --- a/packages/path_provider/path_provider_macos/CHANGELOG.md +++ b/packages/path_provider/path_provider_macos/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 2.0.6 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.0.5 * Removes dependency on `meta`. diff --git a/packages/path_provider/path_provider_macos/example/README.md b/packages/path_provider/path_provider_macos/example/README.md index 4f413873b346..96b8bb17dbff 100644 --- a/packages/path_provider/path_provider_macos/example/README.md +++ b/packages/path_provider/path_provider_macos/example/README.md @@ -1,8 +1,9 @@ -# path_provider_macos_example +# Platform Implementation Test App -Demonstrates how to use the path_provider_macos plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/path_provider/path_provider_macos/example/lib/main.dart b/packages/path_provider/path_provider_macos/example/lib/main.dart index 67a0eb32eeda..13a6fada5fef 100644 --- a/packages/path_provider/path_provider_macos/example/lib/main.dart +++ b/packages/path_provider/path_provider_macos/example/lib/main.dart @@ -8,13 +8,15 @@ import 'package:flutter/material.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } /// Sample app class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { diff --git a/packages/path_provider/path_provider_macos/example/pubspec.yaml b/packages/path_provider/path_provider_macos/example/pubspec.yaml index d8b93545ed53..8c69e69ce122 100644 --- a/packages/path_provider/path_provider_macos/example/pubspec.yaml +++ b/packages/path_provider/path_provider_macos/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -21,9 +21,10 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter - pedantic: ^1.8.0 flutter: uses-material-design: true diff --git a/packages/path_provider/path_provider_macos/macos/path_provider_macos.podspec b/packages/path_provider/path_provider_macos/macos/path_provider_macos.podspec index 1f28f2bf7cf0..14c468231f8c 100644 --- a/packages/path_provider/path_provider_macos/macos/path_provider_macos.podspec +++ b/packages/path_provider/path_provider_macos/macos/path_provider_macos.podspec @@ -11,7 +11,7 @@ Pod::Spec.new do |s| s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_macos' s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_macos' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_macos' } s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' diff --git a/packages/path_provider/path_provider_macos/pubspec.yaml b/packages/path_provider/path_provider_macos/pubspec.yaml index 2451b6dedf80..289e13103ddd 100644 --- a/packages/path_provider/path_provider_macos/pubspec.yaml +++ b/packages/path_provider/path_provider_macos/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider_macos description: macOS implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.5 +version: 2.0.6 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md index 8be2da70c20d..f12e1ec53ade 100644 --- a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md +++ b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md @@ -1,3 +1,13 @@ +## 2.0.5 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. + +## 2.0.4 + +* Minor fixes for new analysis options. +* Removes unnecessary imports. + ## 2.0.3 * Removes dependency on `meta`. diff --git a/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart b/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart index c3e5eccbffba..991be55bce8c 100644 --- a/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart +++ b/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart @@ -4,10 +4,9 @@ import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/services.dart'; -import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:platform/platform.dart'; -import 'enums.dart'; +import '../path_provider_platform_interface.dart'; /// An implementation of [PathProviderPlatform] that uses method channels. class MethodChannelPathProvider extends PathProviderPlatform { @@ -24,6 +23,7 @@ class MethodChannelPathProvider extends PathProviderPlatform { /// This API is only exposed for the unit tests. It should not be used by /// any code outside of the plugin itself. @visibleForTesting + // ignore: use_setters_to_change_properties void setMockPathProviderPlatform(Platform platform) { _platform = platform; } diff --git a/packages/path_provider/path_provider_platform_interface/pubspec.yaml b/packages/path_provider/path_provider_platform_interface/pubspec.yaml index d1b0b3821e21..6ce7ec662b33 100644 --- a/packages/path_provider/path_provider_platform_interface/pubspec.yaml +++ b/packages/path_provider/path_provider_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%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.0.3 +version: 2.0.5 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_windows/CHANGELOG.md b/packages/path_provider/path_provider_windows/CHANGELOG.md index cd33e85a851d..757f13dbb533 100644 --- a/packages/path_provider/path_provider_windows/CHANGELOG.md +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -1,3 +1,27 @@ +## 2.1.3 + +* Updates minimum Flutter version to 2.10. +* Adds compatibility with `package:win32` 3.x. + +## 2.1.2 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 2.1.1 + +* Updates dependency version of `package:win32` to 2.1.0. + +## 2.1.0 + +* Upgrades `package:ffi` dependency to 2.0.0. +* Added support for unicode encoded VERSIONINFO. +* Minor fixes for new analysis options. + +## 2.0.6 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.0.5 * Removes dependency on `meta`. diff --git a/packages/path_provider/path_provider_windows/example/README.md b/packages/path_provider/path_provider_windows/example/README.md index 32f66a86d11d..96b8bb17dbff 100644 --- a/packages/path_provider/path_provider_windows/example/README.md +++ b/packages/path_provider/path_provider_windows/example/README.md @@ -1,8 +1,9 @@ -# path_provider_windows_example +# Platform Implementation Test App -Demonstrates how to use the path_provider_windows plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/path_provider/path_provider_windows/example/lib/main.dart b/packages/path_provider/path_provider_windows/example/lib/main.dart index 509292bf7405..4c63d245a16a 100644 --- a/packages/path_provider/path_provider_windows/example/lib/main.dart +++ b/packages/path_provider/path_provider_windows/example/lib/main.dart @@ -8,13 +8,15 @@ import 'package:flutter/material.dart'; import 'package:path_provider_windows/path_provider_windows.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } /// Sample app class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { diff --git a/packages/path_provider/path_provider_windows/example/pubspec.yaml b/packages/path_provider/path_provider_windows/example/pubspec.yaml index d943347df1ff..d70a4a84f504 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: ">=1.20.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -20,6 +20,8 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugins.cmake b/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugins.cmake index 4d10c2518654..b93c4c30c167 100644 --- a/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugins.cmake +++ b/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -13,3 +16,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart index bfffa848981a..691d7a2da84b 100644 --- a/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart +++ b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart @@ -13,22 +13,44 @@ import 'package:win32/win32.dart'; import 'folders.dart'; +/// Constant for en-US language used in VersionInfo keys. +@visibleForTesting +const String languageEn = '0409'; + +/// Constant for CP1252 encoding used in VersionInfo keys +@visibleForTesting +const String encodingCP1252 = '04e4'; + +/// Constant for Unicode encoding used in VersionInfo keys +@visibleForTesting +const String encodingUnicode = '04b0'; + /// Wraps the Win32 VerQueryValue API call. /// /// This class exists to allow injecting alternate metadata in tests without /// building multiple custom test binaries. @visibleForTesting class VersionInfoQuerier { - /// Returns the value for [key] in [versionInfo]s English strings section, or - /// null if there is no such entry, or if versionInfo is null. - String? getStringValue(Pointer? versionInfo, String key) { + /// Returns the value for [key] in [versionInfo]s in section with given + /// language and encoding, or null if there is no such entry, + /// or if versionInfo is null. + /// + /// See https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource + /// for list of possible language and encoding values. + String? getStringValue( + Pointer? versionInfo, + String key, { + required String language, + required String encoding, + }) { + assert(language.isNotEmpty); + assert(encoding.isNotEmpty); if (versionInfo == null) { return null; } - const String kEnUsLanguageCode = '040904e4'; final Pointer keyPath = - TEXT('\\StringFileInfo\\$kEnUsLanguageCode\\$key'); - final Pointer length = calloc(); + TEXT('\\StringFileInfo\\$language$encoding\\$key'); + final Pointer length = calloc(); final Pointer> valueAddress = calloc>(); try { if (VerQueryValue(versionInfo, keyPath, valueAddress, length) == 0) { @@ -139,7 +161,7 @@ class PathProviderWindows extends PathProviderPlatform { if (hr == E_INVALIDARG || hr == E_FAIL) { throw WindowsException(hr); } - return Future.value(null); + return Future.value(); } final String path = pathPtrPtr.value.toDartString(); @@ -150,6 +172,12 @@ class PathProviderWindows extends PathProviderPlatform { } } + String? _getStringValue(Pointer? infoBuffer, String key) => + versionInfoQuerier.getStringValue(infoBuffer, key, + language: languageEn, encoding: encodingCP1252) ?? + versionInfoQuerier.getStringValue(infoBuffer, key, + language: languageEn, encoding: encodingUnicode); + /// Returns the relative path string to append to the root directory returned /// by Win32 APIs for application storage (such as RoamingAppDir) to get a /// directory that is unique to the application. @@ -164,10 +192,9 @@ class PathProviderWindows extends PathProviderPlatform { String? companyName; String? productName; - final Pointer moduleNameBuffer = - calloc(MAX_PATH + 1).cast(); - final Pointer unused = calloc(); - Pointer? infoBuffer; + final Pointer moduleNameBuffer = wsalloc(MAX_PATH + 1); + final Pointer unused = calloc(); + Pointer? infoBuffer; try { // Get the module name. final int moduleNameLength = @@ -180,17 +207,17 @@ class PathProviderWindows extends PathProviderPlatform { // From that, load the VERSIONINFO resource final int infoSize = GetFileVersionInfoSize(moduleNameBuffer, unused); if (infoSize != 0) { - infoBuffer = calloc(infoSize); + infoBuffer = calloc(infoSize); if (GetFileVersionInfo(moduleNameBuffer, 0, infoSize, infoBuffer) == 0) { calloc.free(infoBuffer); infoBuffer = null; } } - companyName = _sanitizedDirectoryName( - versionInfoQuerier.getStringValue(infoBuffer, 'CompanyName')); - productName = _sanitizedDirectoryName( - versionInfoQuerier.getStringValue(infoBuffer, 'ProductName')); + companyName = + _sanitizedDirectoryName(_getStringValue(infoBuffer, 'CompanyName')); + productName = + _sanitizedDirectoryName(_getStringValue(infoBuffer, 'ProductName')); // If there was no product name, use the executable name. productName ??= diff --git a/packages/path_provider/path_provider_windows/pubspec.yaml b/packages/path_provider/path_provider_windows/pubspec.yaml index 873fa0a6861b..c89e9a833f72 100644 --- a/packages/path_provider/path_provider_windows/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider_windows description: Windows implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.5 +version: 2.1.3 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" flutter: plugin: @@ -16,12 +16,12 @@ flutter: dartPluginClass: PathProviderWindows dependencies: - ffi: ^1.0.0 + ffi: ^2.0.0 flutter: sdk: flutter path: ^1.8.0 path_provider_platform_interface: ^2.0.0 - win32: ^2.0.0 + win32: ">=2.1.0 <4.0.0" dev_dependencies: flutter_test: diff --git a/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart b/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart index e977e07d99e6..48e56406c14f 100644 --- a/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart +++ b/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart @@ -7,15 +7,33 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:path_provider_windows/path_provider_windows.dart'; +import 'package:path_provider_windows/src/path_provider_windows_real.dart' + show languageEn, encodingCP1252, encodingUnicode; // A fake VersionInfoQuerier that just returns preset responses. class FakeVersionInfoQuerier implements VersionInfoQuerier { - FakeVersionInfoQuerier(this.responses); + FakeVersionInfoQuerier( + this.responses, { + this.language = languageEn, + this.encoding = encodingUnicode, + }); + final String language; + final String encoding; final Map responses; - String? getStringValue(Pointer? versionInfo, String key) => - responses[key]; + String? getStringValue( + Pointer? versionInfo, + String key, { + required String language, + required String encoding, + }) { + if (language == this.language && encoding == this.encoding) { + return responses[key]; + } else { + return null; + } + } } void main() { @@ -40,7 +58,21 @@ void main() { expect(path, endsWith(r'flutter_tester')); }, skip: !Platform.isWindows); - test('getApplicationSupportPath with full version info', () async { + test('getApplicationSupportPath with full version info in CP1252', () async { + final PathProviderWindows pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'CompanyName': 'A Company', + 'ProductName': 'Amazing App', + }, encoding: encodingCP1252); + final String? path = await pathProvider.getApplicationSupportPath(); + expect(path, isNotNull); + if (path != null) { + expect(path, endsWith(r'AppData\Roaming\A Company\Amazing App')); + expect(Directory(path).existsSync(), isTrue); + } + }, skip: !Platform.isWindows); + + test('getApplicationSupportPath with full version info in Unicode', () async { final PathProviderWindows pathProvider = PathProviderWindows(); pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ 'CompanyName': 'A Company', @@ -54,6 +86,21 @@ void main() { } }, skip: !Platform.isWindows); + test( + 'getApplicationSupportPath with full version info in Unsupported Encoding', + () async { + final PathProviderWindows pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'CompanyName': 'A Company', + 'ProductName': 'Amazing App', + }, language: '0000', encoding: '0000'); + final String? path = await pathProvider.getApplicationSupportPath(); + expect(path, contains(r'C:\')); + expect(path, contains(r'AppData')); + // The last path component should be the executable name. + expect(path, endsWith(r'flutter_tester')); + }, skip: !Platform.isWindows); + test('getApplicationSupportPath with missing company', () async { final PathProviderWindows pathProvider = PathProviderWindows(); pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ @@ -78,9 +125,8 @@ void main() { if (path != null) { expect( path, - endsWith(r'AppData\Roaming\' - r'A _Bad_ Company_ Name\' - r'A__Terrible__App__Name')); + endsWith( + r'AppData\Roaming\A _Bad_ Company_ Name\A__Terrible__App__Name')); expect(Directory(path).existsSync(), isTrue); } }, skip: !Platform.isWindows); diff --git a/packages/plugin_platform_interface/CHANGELOG.md b/packages/plugin_platform_interface/CHANGELOG.md index 72229cb63410..0b5a6b63a52f 100644 --- a/packages/plugin_platform_interface/CHANGELOG.md +++ b/packages/plugin_platform_interface/CHANGELOG.md @@ -1,6 +1,11 @@ -## NEXT +## 2.1.3 +* Minor fixes for new analysis options. * Adds additional tests for `PlatformInterface` and `MockPlatformInterfaceMixin`. +* Modifies `PlatformInterface` to use an expando for detecting if a customer + tries to implement PlatformInterface using `implements` rather than `extends`. + This ensures that `verify` will continue to work as advertized after + https://github.com/dart-lang/language/issues/2020 is implemented. ## 2.1.2 diff --git a/packages/plugin_platform_interface/lib/plugin_platform_interface.dart b/packages/plugin_platform_interface/lib/plugin_platform_interface.dart index a03c9ce2d367..6733b29953b0 100644 --- a/packages/plugin_platform_interface/lib/plugin_platform_interface.dart +++ b/packages/plugin_platform_interface/lib/plugin_platform_interface.dart @@ -44,9 +44,20 @@ abstract class PlatformInterface { /// derived classes. /// /// @param token The same, non-`const` `Object` that will be passed to `verify`. - PlatformInterface({required Object token}) : _instanceToken = token; + PlatformInterface({required Object token}) { + _instanceTokens[this] = token; + } - final Object? _instanceToken; + /// Expando mapping instances of PlatformInterface to their associated tokens. + /// The reason this is not simply a private field of type `Object?` is because + /// as of the implementation of field promotion in Dart + /// (https://github.com/dart-lang/language/issues/2020), it is a runtime error + /// to invoke a private member that is mocked in another library. The expando + /// approach prevents [_verify] from triggering this runtime exception when + /// encountering an implementation that uses `implements` rather than + /// `extends`. This in turn allows [_verify] to throw an [AssertionError] (as + /// documented). + static final Expando _instanceTokens = Expando(); /// Ensures that the platform instance was constructed with a non-`const` token /// that matches the provided token and throws [AssertionError] if not. @@ -89,10 +100,10 @@ abstract class PlatformInterface { return; } if (preventConstObject && - identical(instance._instanceToken, const Object())) { + identical(_instanceTokens[instance], const Object())) { throw AssertionError('`const Object()` cannot be used as the token.'); } - if (!identical(token, instance._instanceToken)) { + if (!identical(token, _instanceTokens[instance])) { throw AssertionError( 'Platform interfaces must not be implemented with `implements`'); } diff --git a/packages/plugin_platform_interface/pubspec.yaml b/packages/plugin_platform_interface/pubspec.yaml index 1b601db1a388..6a4bc488693b 100644 --- a/packages/plugin_platform_interface/pubspec.yaml +++ b/packages/plugin_platform_interface/pubspec.yaml @@ -15,7 +15,7 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ # be done when absolutely necessary and after the ecosystem has already migrated to 2.X.Y version # that is forward compatible with 3.0.0 (ideally the ecosystem have migrated to depend on: # `plugin_platform_interface: >=2.X.Y <4.0.0`). -version: 2.1.2 +version: 2.1.3 environment: sdk: ">=2.12.0 <3.0.0" @@ -25,5 +25,4 @@ dependencies: dev_dependencies: mockito: ^5.0.0 - pedantic: ^1.10.0 test: ^1.16.0 diff --git a/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart index 9e1ddc09e92b..869017cd4f23 100644 --- a/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart +++ b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart @@ -11,6 +11,7 @@ class SamplePluginPlatform extends PlatformInterface { static final Object _token = Object(); + // ignore: avoid_setters_without_getters static set instance(SamplePluginPlatform instance) { PlatformInterface.verify(instance, _token); // A real implementation would set a static instance field here. @@ -20,6 +21,12 @@ class SamplePluginPlatform extends PlatformInterface { class ImplementsSamplePluginPlatform extends Mock implements SamplePluginPlatform {} +class ImplementsSamplePluginPlatformUsingNoSuchMethod + implements SamplePluginPlatform { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + class ImplementsSamplePluginPlatformUsingMockPlatformInterfaceMixin extends Mock with MockPlatformInterfaceMixin implements SamplePluginPlatform {} @@ -35,6 +42,7 @@ class ConstTokenPluginPlatform extends PlatformInterface { static const Object _token = Object(); // invalid + // ignore: avoid_setters_without_getters static set instance(ConstTokenPluginPlatform instance) { PlatformInterface.verify(instance, _token); } @@ -47,6 +55,7 @@ class VerifyTokenPluginPlatform extends PlatformInterface { static final Object _token = Object(); + // ignore: avoid_setters_without_getters static set instance(VerifyTokenPluginPlatform instance) { PlatformInterface.verifyToken(instance, _token); // A real implementation would set a static instance field here. @@ -68,6 +77,7 @@ class ConstVerifyTokenPluginPlatform extends PlatformInterface { static const Object _token = Object(); // invalid + // ignore: avoid_setters_without_getters static set instance(ConstVerifyTokenPluginPlatform instance) { PlatformInterface.verifyToken(instance, _token); } @@ -94,6 +104,13 @@ void main() { }, throwsA(isA())); }); + test('prevents implmentation with `implements` and `noSuchMethod`', () { + expect(() { + SamplePluginPlatform.instance = + ImplementsSamplePluginPlatformUsingNoSuchMethod(); + }, throwsA(isA())); + }); + test('allows mocking with `implements`', () { final SamplePluginPlatform mock = ImplementsSamplePluginPlatformUsingMockPlatformInterfaceMixin(); diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index 33e80498a63c..7d1881596255 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,3 +1,27 @@ +## 1.0.1 + +* Updates implementaion package versions to current versions. + +## 1.0.0 + +* Updates version to 1.0 to reflect current status. +* Updates minimum Flutter version to 2.10. +* Updates README to document that on Android, icons may need to be explicitly + marked as used in the Android project for release builds. +* Minor fixes for new analysis options. + +## 0.6.0+11 + +* Removes unnecessary imports. +* Updates minimum Flutter version to 2.8. +* Adds OS version support information to README. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 0.6.0+10 + +* Moves Android and iOS implementations to federated packages. + ## 0.6.0+9 * Updates Android compileSdkVersion to 31. diff --git a/packages/quick_actions/quick_actions/README.md b/packages/quick_actions/quick_actions/README.md index 46e87fa0b241..3b5bcbaa64ef 100644 --- a/packages/quick_actions/quick_actions/README.md +++ b/packages/quick_actions/quick_actions/README.md @@ -7,10 +7,13 @@ Quick actions refer to the [eponymous concept](https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/home-screen-actions/) on iOS and to the [App Shortcuts](https://developer.android.com/guide/topics/ui/shortcuts.html) APIs on -Android (introduced in Android 7.1 / API level 25). It is safe to run this plugin -with earlier versions of Android as it will produce a noop. +Android. -## Usage in Dart +| | Android | iOS | +|-------------|-----------|------| +| **Support** | SDK 16+\* | 9.0+ | + +## Usage Initialize the library early in your application's lifecycle by providing a callback, which will then be called whenever the user launches the app via a @@ -40,9 +43,12 @@ Please note, that the `type` argument should be unique within your application name of the native resource (xcassets on iOS or drawable on Android) that the app will display for the quick action. -## Getting Started +### Android -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +\* The plugin will compile and run on SDK 16+, but will be a no-op below SDK 25 +(Android 7.1). -For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). +If the drawables used as icons are not referenced other than in your Dart code, +you may need to +[explicitly mark them to be kept](https://developer.android.com/studio/build/shrink-code#keep-resources) +to ensure that they will be available for use in release builds. diff --git a/packages/quick_actions/quick_actions/example/README.md b/packages/quick_actions/quick_actions/example/README.md index d1b72891de9e..c8a629019fc9 100644 --- a/packages/quick_actions/quick_actions/example/README.md +++ b/packages/quick_actions/quick_actions/example/README.md @@ -1,8 +1,3 @@ # quick_actions_example Demonstrates how to use the quick_actions plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/quick_actions/quick_actions/example/android/app/build.gradle b/packages/quick_actions/quick_actions/example/android/app/build.gradle index 54f2e59bacf7..75fe3543e987 100644 --- a/packages/quick_actions/quick_actions/example/android/app/build.gradle +++ b/packages/quick_actions/quick_actions/example/android/app/build.gradle @@ -52,7 +52,7 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' api 'androidx.test:core:1.2.0' diff --git a/packages/quick_actions/quick_actions/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/quick_actions/quick_actions/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/quick_actions/quick_actions/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/quick_actions/quick_actions/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/quick_actions/quick_actions/example/android/build.gradle b/packages/quick_actions/quick_actions/example/android/build.gradle index e101ac08df55..c21bff8e0a2f 100644 --- a/packages/quick_actions/quick_actions/example/android/build.gradle +++ b/packages/quick_actions/quick_actions/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:7.0.1' } } diff --git a/packages/quick_actions/quick_actions/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/quick_actions/quick_actions/example/android/gradle/wrapper/gradle-wrapper.properties index 019065d1d650..297f2fec363f 100644 --- a/packages/quick_actions/quick_actions/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/quick_actions/quick_actions/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/quick_actions/quick_actions/example/integration_test/quick_actions_test.dart b/packages/quick_actions/quick_actions/example/integration_test/quick_actions_test.dart index bfefef3b298b..37846c323591 100644 --- a/packages/quick_actions/quick_actions/example/integration_test/quick_actions_test.dart +++ b/packages/quick_actions/quick_actions/example/integration_test/quick_actions_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.9 import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:quick_actions/quick_actions.dart'; @@ -12,7 +11,7 @@ void main() { testWidgets('Can set shortcuts', (WidgetTester tester) async { const QuickActions quickActions = QuickActions(); - await quickActions.initialize(null); + await quickActions.initialize((String _) {}); const ShortcutItem shortCutItem = ShortcutItem( type: 'action_one', diff --git a/packages/quick_actions/quick_actions/example/lib/main.dart b/packages/quick_actions/quick_actions/example/lib/main.dart index 1ce6f51d71de..cafbf0c351d9 100644 --- a/packages/quick_actions/quick_actions/example/lib/main.dart +++ b/packages/quick_actions/quick_actions/example/lib/main.dart @@ -8,10 +8,12 @@ import 'package:flutter/material.dart'; import 'package:quick_actions/quick_actions.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -28,7 +30,7 @@ class MyHomePage extends StatefulWidget { const MyHomePage({Key? key}) : super(key: key); @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { diff --git a/packages/quick_actions/quick_actions/example/pubspec.yaml b/packages/quick_actions/quick_actions/example/pubspec.yaml index c4ee86039761..1a10a653db06 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: ">=1.9.1+hotfix.2" + flutter: ">=2.10.0" dependencies: flutter: @@ -18,12 +18,13 @@ dependencies: path: ../ dev_dependencies: - espresso: ^0.1.0+2 + espresso: ^0.2.0 flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/quick_actions/quick_actions/example/test_driver/integration_test.dart b/packages/quick_actions/quick_actions/example/test_driver/integration_test.dart index 6a0e6fa82dbe..4f10f2a522f3 100644 --- a/packages/quick_actions/quick_actions/example/test_driver/integration_test.dart +++ b/packages/quick_actions/quick_actions/example/test_driver/integration_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart=2.9 - import 'package:integration_test/integration_test_driver.dart'; Future main() => integrationDriver(); diff --git a/packages/quick_actions/quick_actions/pubspec.yaml b/packages/quick_actions/quick_actions/pubspec.yaml index b2a9f7498a95..08b486fe50e3 100644 --- a/packages/quick_actions/quick_actions/pubspec.yaml +++ b/packages/quick_actions/quick_actions/pubspec.yaml @@ -3,15 +3,11 @@ description: Flutter plugin for creating shortcuts on home screen, also known as Quick Actions on iOS and App Shortcuts on Android. repository: https://github.com/flutter/plugins/tree/main/packages/quick_actions/quick_actions issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+quick_actions%22 -version: 0.6.0+9 - -# Temporarily disable publishing to allow moving Android and iOS -# implementations. -publish_to: none +version: 1.0.1 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: @@ -24,11 +20,8 @@ flutter: dependencies: flutter: sdk: flutter - # Temporary path dependencies to allow moving Android and iOS implementations. - quick_actions_android: - path: ../quick_actions_android - quick_actions_ios: - path: ../quick_actions_ios + quick_actions_android: ^1.0.0 + quick_actions_ios: ^1.0.0 quick_actions_platform_interface: ^1.0.0 dev_dependencies: @@ -36,6 +29,5 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - mockito: ^5.0.0-nullsafety.7 - pedantic: ^1.11.0 + mockito: ^5.0.0 plugin_platform_interface: ^2.0.0 diff --git a/packages/quick_actions/quick_actions/test/quick_actions_test.dart b/packages/quick_actions/quick_actions/test/quick_actions_test.dart index 09fcc9799c11..be9fd5e7720a 100644 --- a/packages/quick_actions/quick_actions/test/quick_actions_test.dart +++ b/packages/quick_actions/quick_actions/test/quick_actions_test.dart @@ -6,9 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:quick_actions/quick_actions.dart'; -import 'package:quick_actions_platform_interface/platform_interface/quick_actions_platform.dart'; import 'package:quick_actions_platform_interface/quick_actions_platform_interface.dart'; -import 'package:quick_actions_platform_interface/types/shortcut_item.dart'; void main() { group('$QuickActions', () { @@ -23,7 +21,7 @@ void main() { test('initialize() PlatformInterface', () async { const QuickActions quickActions = QuickActions(); - final QuickActionHandler handler = (String type) {}; + void handler(String type) {} await quickActions.initialize(handler); verify(QuickActionsPlatform.instance.initialize(handler)).called(1); @@ -31,7 +29,7 @@ void main() { test('setShortcutItems() PlatformInterface', () { const QuickActions quickActions = QuickActions(); - final QuickActionHandler handler = (String type) {}; + void handler(String type) {} quickActions.initialize(handler); quickActions.setShortcutItems([]); @@ -42,7 +40,7 @@ void main() { test('clearShortcutItems() PlatformInterface', () { const QuickActions quickActions = QuickActions(); - final QuickActionHandler handler = (String type) {}; + void handler(String type) {} quickActions.initialize(handler); quickActions.clearShortcutItems(); diff --git a/packages/quick_actions/quick_actions_android/CHANGELOG.md b/packages/quick_actions/quick_actions_android/CHANGELOG.md index 98e8cf5e333b..bc809a4dc477 100644 --- a/packages/quick_actions/quick_actions_android/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_android/CHANGELOG.md @@ -1,3 +1,27 @@ +## 1.0.0 + +* Updates version to 1.0 to reflect current status. +* Updates minimum Flutter version to 2.10. +* Updates mockito-core to 4.6.1. +* Removes deprecated FieldSetter from QuickActionsTest. + +## 0.6.2 + +* Updates gradle version to 7.2.1. + +## 0.6.1 + +* Allows Android to trigger quick actions without restarting the app. + +## 0.6.0+11 + +* Updates references to the obsolete master branch. + +## 0.6.0+10 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 0.6.0+9 -* Switches to a package-internal implementation of the platform interface. \ No newline at end of file +* Switches to a package-internal implementation of the platform interface. diff --git a/packages/quick_actions/quick_actions_android/README.md b/packages/quick_actions/quick_actions_android/README.md index caeb94374398..8b7fc8895212 100644 --- a/packages/quick_actions/quick_actions_android/README.md +++ b/packages/quick_actions/quick_actions_android/README.md @@ -13,5 +13,5 @@ If you would like to contribute to the plugin, check out our [contribution guide [1]: https://pub.dev/packages/quick_actions [2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin -[3]: https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md +[3]: https://github.com/flutter/plugins/blob/main/CONTRIBUTING.md diff --git a/packages/quick_actions/quick_actions_android/android/build.gradle b/packages/quick_actions/quick_actions_android/android/build.gradle index 590bc6b4a826..e4cdec819ec9 100644 --- a/packages/quick_actions/quick_actions_android/android/build.gradle +++ b/packages/quick_actions/quick_actions_android/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:7.2.1' } } @@ -29,13 +29,15 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { + // TODO(stuartmorgan): Enable when gradle is updated. + // disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' } dependencies { - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:3.2.4' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:4.7.0' } compileOptions { diff --git a/packages/quick_actions/quick_actions_android/android/src/main/AndroidManifest.xml b/packages/quick_actions/quick_actions_android/android/src/main/AndroidManifest.xml index 5b02f6d8aef2..5ec81f08ec6a 100644 --- a/packages/quick_actions/quick_actions_android/android/src/main/AndroidManifest.xml +++ b/packages/quick_actions/quick_actions_android/android/src/main/AndroidManifest.xml @@ -1,4 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools" + package="io.flutter.plugins.quickactions"> + + diff --git a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java index 6316e8428288..96b141fb9c31 100644 --- a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java +++ b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java @@ -74,7 +74,8 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { final boolean didSucceed = dynamicShortcutsSet; - // TODO(camsim99): Move re-dispatch below to background thread when Flutter 2.8+ is stable. + // TODO(camsim99): Move re-dispatch below to background thread when Flutter 2.8+ is + // stable. uiThreadExecutor.execute( () -> { if (didSucceed) { @@ -163,7 +164,7 @@ private Intent getIntentToOpenMainActivity(String type) { .setAction(Intent.ACTION_RUN) .putExtra(EXTRA_ACTION, type) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); } private static class UiThreadExecutor implements Executor { diff --git a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java index 99ce0f8426a0..b41087816889 100644 --- a/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java +++ b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java @@ -7,6 +7,7 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.pm.ShortcutManager; import android.os.Build; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; @@ -21,6 +22,7 @@ public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewInte private MethodChannel channel; private MethodCallHandlerImpl handler; + private Activity activity; /** * Plugin registration. @@ -45,9 +47,10 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { @Override public void onAttachedToActivity(ActivityPluginBinding binding) { - handler.setActivity(binding.getActivity()); + activity = binding.getActivity(); + handler.setActivity(activity); binding.addOnNewIntentListener(this); - onNewIntent(binding.getActivity().getIntent()); + onNewIntent(activity.getIntent()); } @Override @@ -74,7 +77,12 @@ public boolean onNewIntent(Intent intent) { } // Notify the Dart side if the launch intent has the intent extra relevant to quick actions. if (intent.hasExtra(MethodCallHandlerImpl.EXTRA_ACTION) && channel != null) { - channel.invokeMethod("launch", intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION)); + Context context = activity.getApplicationContext(); + ShortcutManager shortcutManager = + (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); + String shortcutId = intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION); + channel.invokeMethod("launch", shortcutId); + shortcutManager.reportShortcutUsed(shortcutId); } return false; } diff --git a/packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java b/packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java index d2e63b62f229..911b789190ab 100644 --- a/packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java +++ b/packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java @@ -13,7 +13,9 @@ import static org.mockito.Mockito.when; import android.app.Activity; +import android.content.Context; import android.content.Intent; +import android.content.pm.ShortcutManager; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -27,7 +29,6 @@ import java.nio.ByteBuffer; import org.junit.After; import org.junit.Test; -import org.mockito.internal.util.reflection.FieldSetter; public class QuickActionsTest { private static class TestBinaryMessenger implements BinaryMessenger { @@ -77,15 +78,19 @@ public void onAttachedToActivity_buildVersionSupported_invokesLaunchMethod() final QuickActionsPlugin plugin = new QuickActionsPlugin(); setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); setBuildVersion(SUPPORTED_BUILD); - FieldSetter.setField( - plugin, - QuickActionsPlugin.class.getDeclaredField("handler"), - mock(MethodCallHandlerImpl.class)); + Field handler = plugin.getClass().getDeclaredField("handler"); + handler.setAccessible(true); + handler.set(plugin, mock(MethodCallHandlerImpl.class)); final Intent mockIntent = createMockIntentWithQuickActionExtra(); final Activity mockMainActivity = mock(Activity.class); when(mockMainActivity.getIntent()).thenReturn(mockIntent); final ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class); when(mockActivityPluginBinding.getActivity()).thenReturn(mockMainActivity); + final Context mockContext = mock(Context.class); + when(mockMainActivity.getApplicationContext()).thenReturn(mockContext); + final ShortcutManager mockShortcutManager = mock(ShortcutManager.class); + when(mockContext.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(mockShortcutManager); + plugin.onAttachedToActivity(mockActivityPluginBinding); // Act plugin.onAttachedToActivity(mockActivityPluginBinding); @@ -123,6 +128,15 @@ public void onNewIntent_buildVersionSupported_invokesLaunchMethod() setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); setBuildVersion(SUPPORTED_BUILD); final Intent mockIntent = createMockIntentWithQuickActionExtra(); + final Activity mockMainActivity = mock(Activity.class); + when(mockMainActivity.getIntent()).thenReturn(mockIntent); + final ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class); + when(mockActivityPluginBinding.getActivity()).thenReturn(mockMainActivity); + final Context mockContext = mock(Context.class); + when(mockMainActivity.getApplicationContext()).thenReturn(mockContext); + final ShortcutManager mockShortcutManager = mock(ShortcutManager.class); + when(mockContext.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(mockShortcutManager); + plugin.onAttachedToActivity(mockActivityPluginBinding); // Act final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent); diff --git a/packages/quick_actions/quick_actions_android/example/README.md b/packages/quick_actions/quick_actions_android/example/README.md index d1b72891de9e..96b8bb17dbff 100644 --- a/packages/quick_actions/quick_actions_android/example/README.md +++ b/packages/quick_actions/quick_actions_android/example/README.md @@ -1,8 +1,9 @@ -# quick_actions_example +# Platform Implementation Test App -Demonstrates how to use the quick_actions plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. 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 54f2e59bacf7..666194bc11b0 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 @@ -24,8 +24,10 @@ if (flutterVersionName == null) { apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +def androidXTestVersion = '1.2.0' + android { - compileSdkVersion 31 + compileSdkVersion 32 lintOptions { disable 'InvalidPackage' @@ -52,8 +54,14 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - api 'androidx.test:core:1.2.0' + api "androidx.test:core:$androidXTestVersion" + + 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' } diff --git a/packages/quick_actions/quick_actions_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/quick_actions/quick_actions_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/quick_actions/quick_actions_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/quick_actions/quick_actions_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java b/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java index 9d2fed13fc27..cfcef3e1c76f 100644 --- a/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java +++ b/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java @@ -4,15 +4,56 @@ package io.flutter.plugins.quickactionsexample; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.util.Log; +import androidx.lifecycle.Lifecycle; import androidx.test.core.app.ActivityScenario; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.Until; import io.flutter.plugins.quickactions.QuickActionsPlugin; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; +import org.junit.runner.RunWith; +@RunWith(AndroidJUnit4.class) public class QuickActionsTest { + private Context context; + private UiDevice device; + private ActivityScenario scenario; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + scenario = ensureAppRunToView(); + ensureAllAppShortcutsAreCreated(); + } + + @After + public void tearDown() { + scenario.close(); + Log.i(QuickActionsTest.class.getSimpleName(), "Run to completion"); + } + @Test - public void imagePickerPluginIsAdded() { + public void quickActionPluginIsAdded() { final ActivityScenario scenario = ActivityScenario.launch(QuickActionsTestActivity.class); scenario.onActivity( @@ -20,4 +61,98 @@ public void imagePickerPluginIsAdded() { assertTrue(activity.engine.getPlugins().has(QuickActionsPlugin.class)); }); } + + @Test + public void appShortcutsAreCreated() { + List expectedShortcuts = createMockShortcuts(); + + ShortcutManager shortcutManager = + (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); + List dynamicShortcuts = shortcutManager.getDynamicShortcuts(); + + // Assert the app shortcuts defined in ../lib/main.dart. + assertFalse(dynamicShortcuts.isEmpty()); + assertEquals(expectedShortcuts.size(), dynamicShortcuts.size()); + for (ShortcutInfo expectedShortcut : expectedShortcuts) { + ShortcutInfo dynamicShortcut = + dynamicShortcuts + .stream() + .filter(s -> s.getId().equals(expectedShortcut.getId())) + .findFirst() + .get(); + + assertEquals(expectedShortcut.getShortLabel(), dynamicShortcut.getShortLabel()); + assertEquals(expectedShortcut.getLongLabel(), dynamicShortcut.getLongLabel()); + } + } + + // TODO(bparrishMines): The test is ignored because it fails when ran on Firebase Test Lab. See + // https://github.com/flutter/flutter/issues/114246. + @Ignore + @Test + public void appShortcutLaunchActivityAfterStarting() { + // Arrange + List shortcuts = createMockShortcuts(); + ShortcutInfo firstShortcut = shortcuts.get(0); + ShortcutManager shortcutManager = + (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); + List dynamicShortcuts = shortcutManager.getDynamicShortcuts(); + ShortcutInfo dynamicShortcut = + dynamicShortcuts + .stream() + .filter(s -> s.getId().equals(firstShortcut.getId())) + .findFirst() + .get(); + Intent dynamicShortcutIntent = dynamicShortcut.getIntent(); + AtomicReference initialActivity = new AtomicReference<>(); + scenario.onActivity(initialActivity::set); + String appReadySentinel = " has launched"; + + // Act + context.startActivity(dynamicShortcutIntent); + device.wait(Until.hasObject(By.descContains(appReadySentinel)), 2000); + AtomicReference currentActivity = new AtomicReference<>(); + scenario.onActivity(currentActivity::set); + + // Assert + Assert.assertTrue( + "AppShortcut:" + firstShortcut.getId() + " does not launch the correct activity", + // We can only find the shortcut type in content description while inspecting it in Ui + // Automator Viewer. + device.hasObject(By.desc(firstShortcut.getId() + appReadySentinel))); + // This is Android SingleTop behavior in which Android does not destroy the initial activity and + // launch a new activity. + Assert.assertEquals(initialActivity.get(), currentActivity.get()); + } + + private void ensureAllAppShortcutsAreCreated() { + device.wait(Until.hasObject(By.text("actions ready")), 1000); + } + + private List createMockShortcuts() { + List expectedShortcuts = new ArrayList<>(); + + String actionOneLocalizedTitle = "Action one"; + expectedShortcuts.add( + new ShortcutInfo.Builder(context, "action_one") + .setShortLabel(actionOneLocalizedTitle) + .setLongLabel(actionOneLocalizedTitle) + .build()); + + String actionTwoLocalizedTitle = "Action two"; + expectedShortcuts.add( + new ShortcutInfo.Builder(context, "action_two") + .setShortLabel(actionTwoLocalizedTitle) + .setLongLabel(actionTwoLocalizedTitle) + .build()); + + return expectedShortcuts; + } + + private ActivityScenario ensureAppRunToView() { + final ActivityScenario scenario = + ActivityScenario.launch(QuickActionsTestActivity.class); + scenario.moveToState(Lifecycle.State.STARTED); + return scenario; + } } diff --git a/packages/quick_actions/quick_actions_android/example/android/build.gradle b/packages/quick_actions/quick_actions_android/example/android/build.gradle index e101ac08df55..c21bff8e0a2f 100644 --- a/packages/quick_actions/quick_actions_android/example/android/build.gradle +++ b/packages/quick_actions/quick_actions_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:7.0.1' } } diff --git a/packages/quick_actions/quick_actions_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/quick_actions/quick_actions_android/example/android/gradle/wrapper/gradle-wrapper.properties index 019065d1d650..297f2fec363f 100644 --- a/packages/quick_actions/quick_actions_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/quick_actions/quick_actions_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/quick_actions/quick_actions_android/example/integration_test/quick_actions_test.dart b/packages/quick_actions/quick_actions_android/example/integration_test/quick_actions_test.dart index f9c42ad109e7..e0abe90f75aa 100644 --- a/packages/quick_actions/quick_actions_android/example/integration_test/quick_actions_test.dart +++ b/packages/quick_actions/quick_actions_android/example/integration_test/quick_actions_test.dart @@ -2,23 +2,21 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:quick_actions_platform_interface/quick_actions_platform_interface.dart'; +import 'package:quick_actions_example/main.dart' as app; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Can set shortcuts', (WidgetTester tester) async { - final QuickActionsPlatform quickActions = QuickActionsPlatform.instance; - await quickActions.initialize((String value) {}); + testWidgets('Can run MyApp', (WidgetTester tester) async { + app.main(); - const ShortcutItem shortCutItem = ShortcutItem( - type: 'action_one', - localizedTitle: 'Action one', - icon: 'AppIcon', - ); - expect( - quickActions.setShortcutItems([shortCutItem]), completes); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 1)); + + expect(find.byType(Text), findsWidgets); + expect(find.byType(app.MyHomePage), findsOneWidget); }); } diff --git a/packages/quick_actions/quick_actions_android/example/lib/main.dart b/packages/quick_actions/quick_actions_android/example/lib/main.dart index 06f141073b33..8f66e69ffb4e 100644 --- a/packages/quick_actions/quick_actions_android/example/lib/main.dart +++ b/packages/quick_actions/quick_actions_android/example/lib/main.dart @@ -8,10 +8,12 @@ import 'package:flutter/material.dart'; import 'package:quick_actions_android/quick_actions_android.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -28,7 +30,7 @@ class MyHomePage extends StatefulWidget { const MyHomePage({Key? key}) : super(key: key); @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { @@ -42,7 +44,7 @@ class _MyHomePageState extends State { quickActions.initialize((String shortcutType) { setState(() { if (shortcutType != null) { - shortcut = shortcutType; + shortcut = '$shortcutType has launched'; } }); }); diff --git a/packages/quick_actions/quick_actions_android/example/pubspec.yaml b/packages/quick_actions/quick_actions_android/example/pubspec.yaml index 53170971d136..c560d4dd5f1e 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.8.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -18,9 +18,11 @@ dependencies: path: ../ dev_dependencies: - espresso: ^0.1.0+2 + espresso: ^0.2.0 flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/quick_actions/quick_actions_android/pubspec.yaml b/packages/quick_actions/quick_actions_android/pubspec.yaml index cf9971dca945..e47a1fdc13e9 100644 --- a/packages/quick_actions/quick_actions_android/pubspec.yaml +++ b/packages/quick_actions/quick_actions_android/pubspec.yaml @@ -2,11 +2,11 @@ name: quick_actions_android description: An implementation for the Android platform of the Flutter `quick_actions` plugin. repository: https://github.com/flutter/plugins/tree/main/packages/quick_actions/quick_actions_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.6.0+9 +version: 1.0.0 environment: sdk: ">=2.15.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/quick_actions/quick_actions_ios/CHANGELOG.md b/packages/quick_actions/quick_actions_ios/CHANGELOG.md index d48afbd8d13a..31fe43832d2f 100644 --- a/packages/quick_actions/quick_actions_ios/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_ios/CHANGELOG.md @@ -1,4 +1,39 @@ +## NEXT + +* Migrates `RunnerUITests` to Swift. + +## 1.0.1 + +* Removes custom modulemap file with "Test" submodule and private headers for Swift migration. +* Migrates `FLTQuickActionsPlugin` class to Swift. + +## 1.0.0 + +* Updates version to 1.0 to reflect current status. +* Updates minimum Flutter version to 2.10. + +## 0.6.0+14 + +* Refactors `FLTQuickActionsPlugin` class into multiple components. +* Increases unit tests coverage to 100%. + +## 0.6.0+13 + +* Adds some unit tests for `FLTQuickActionsPlugin` class. + +## 0.6.0+12 + +* Adds a custom module map with a Test submodule for unit tests on iOS platform. + +## 0.6.0+11 + +* Updates references to the obsolete master branch. + +## 0.6.0+10 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. ## 0.6.0+9 -* Switches to a package-internal implementation of the platform interface. \ No newline at end of file +* Switches to a package-internal implementation of the platform interface. diff --git a/packages/quick_actions/quick_actions_ios/README.md b/packages/quick_actions/quick_actions_ios/README.md index a0c369813a4d..e33b9ec3ab14 100644 --- a/packages/quick_actions/quick_actions_ios/README.md +++ b/packages/quick_actions/quick_actions_ios/README.md @@ -13,4 +13,4 @@ If you would like to contribute to the plugin, check out our [contribution guide [1]: https://pub.dev/packages/quick_actions [2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin -[3]: https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md \ No newline at end of file +[3]: https://github.com/flutter/plugins/blob/main/CONTRIBUTING.md diff --git a/packages/quick_actions/quick_actions_ios/example/README.md b/packages/quick_actions/quick_actions_ios/example/README.md index d1b72891de9e..96b8bb17dbff 100644 --- a/packages/quick_actions/quick_actions_ios/example/README.md +++ b/packages/quick_actions/quick_actions_ios/example/README.md @@ -1,8 +1,9 @@ -# quick_actions_example +# Platform Implementation Test App -Demonstrates how to use the quick_actions plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Podfile b/packages/quick_actions/quick_actions_ios/example/ios/Podfile index 3924e59aa0f9..b52805241c18 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Podfile +++ b/packages/quick_actions/quick_actions_ios/example/ios/Podfile @@ -31,6 +31,7 @@ target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths + pod 'OCMock', '~> 3.9.1' end end diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj index 36dc0d81923b..c853a197ca9b 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,21 +3,22 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 33E20B3526EFCDFC00A4A191 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33E20B3426EFCDFC00A4A191 /* RunnerTests.m */; }; + 2632072169FF635893D8EB4D /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 436668746754BEEA28B76E55 /* libPods-RunnerTests.a */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 50EB54C1FE43DB743F5DEC7C /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1A69703A518C37D73BF8B91 /* libPods-RunnerTests.a */; }; - 686BE83025E58CCF00862533 /* RunnerUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 686BE82F25E58CCF00862533 /* RunnerUITests.m */; }; - 83C36CAF23D629E5ABE75B2A /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */; }; + 6A841C2B6AED5CF8DB2A1894 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C35AD3650AB6BF850E016715 /* libPods-Runner.a */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + E092A7F628D128EB005C7F67 /* RunnerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E092A7F528D128EB005C7F67 /* RunnerUITests.swift */; }; + E0C09C29289C729D00E6977E /* FLTQuickActionsPluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C09C28289C729D00E6977E /* FLTQuickActionsPluginTests.m */; }; + E0C09C32289DBFCA00E6977E /* FLTShortcutStateManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C09C31289DBFCA00E6977E /* FLTShortcutStateManagerTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,12 +55,11 @@ 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 = ""; }; 33E20B3226EFCDFC00A4A191 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 33E20B3426EFCDFC00A4A191 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; }; 33E20B3626EFCDFC00A4A191 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 436668746754BEEA28B76E55 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 5278439583922091276A37C9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 686BE82F25E58CCF00862533 /* RunnerUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerUITests.m; sourceTree = ""; }; 686BE83125E58CCF00862533 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; @@ -74,8 +74,10 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9D27FE1F0F21D4D47DDA16DE /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - D1A69703A518C37D73BF8B91 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + C35AD3650AB6BF850E016715 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + E092A7F528D128EB005C7F67 /* RunnerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerUITests.swift; sourceTree = ""; }; + E0C09C28289C729D00E6977E /* FLTQuickActionsPluginTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTQuickActionsPluginTests.m; sourceTree = ""; }; + E0C09C31289DBFCA00E6977E /* FLTShortcutStateManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTShortcutStateManagerTests.m; sourceTree = ""; }; F0609304FBCAEC2289164BD5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -84,7 +86,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 50EB54C1FE43DB743F5DEC7C /* libPods-RunnerTests.a in Frameworks */, + 2632072169FF635893D8EB4D /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,7 +101,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 83C36CAF23D629E5ABE75B2A /* libPods-Runner.a in Frameworks */, + 6A841C2B6AED5CF8DB2A1894 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -109,8 +111,9 @@ 33E20B3326EFCDFC00A4A191 /* RunnerTests */ = { isa = PBXGroup; children = ( - 33E20B3426EFCDFC00A4A191 /* RunnerTests.m */, 33E20B3626EFCDFC00A4A191 /* Info.plist */, + E0C09C31289DBFCA00E6977E /* FLTShortcutStateManagerTests.m */, + E0C09C28289C729D00E6977E /* FLTQuickActionsPluginTests.m */, ); path = RunnerTests; sourceTree = ""; @@ -118,8 +121,8 @@ 686BE82E25E58CCF00862533 /* RunnerUITests */ = { isa = PBXGroup; children = ( - 686BE82F25E58CCF00862533 /* RunnerUITests.m */, 686BE83125E58CCF00862533 /* Info.plist */, + E092A7F528D128EB005C7F67 /* RunnerUITests.swift */, ); path = RunnerUITests; sourceTree = ""; @@ -185,8 +188,8 @@ A44AD0D63DEF785A2A2DEE28 /* Frameworks */ = { isa = PBXGroup; children = ( - CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */, - D1A69703A518C37D73BF8B91 /* libPods-RunnerTests.a */, + C35AD3650AB6BF850E016715 /* libPods-Runner.a */, + 436668746754BEEA28B76E55 /* libPods-RunnerTests.a */, ); name = Frameworks; sourceTree = ""; @@ -278,6 +281,7 @@ }; 686BE82C25E58CCF00862533 = { CreatedOnToolsVersion = 12.4; + LastSwiftMigration = 1330; ProvisioningStyle = Automatic; TestTargetID = 97C146ED1CF9000F007C117D; }; @@ -337,6 +341,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -373,6 +378,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -410,7 +416,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 33E20B3526EFCDFC00A4A191 /* RunnerTests.m in Sources */, + E0C09C32289DBFCA00E6977E /* FLTShortcutStateManagerTests.m in Sources */, + E0C09C29289C729D00E6977E /* FLTQuickActionsPluginTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -418,7 +425,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 686BE83025E58CCF00862533 /* RunnerUITests.m in Sources */, + E092A7F628D128EB005C7F67 /* RunnerUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -473,7 +480,11 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; 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"; @@ -486,7 +497,11 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; 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"; @@ -498,6 +513,7 @@ buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; @@ -506,11 +522,17 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = Runner; }; @@ -521,6 +543,7 @@ buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; @@ -529,10 +552,15 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = Runner; }; @@ -655,7 +683,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", @@ -676,7 +707,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", diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist index d6bca84ca23d..2128c14bb939 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist @@ -45,5 +45,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTQuickActionsPluginTests.m b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTQuickActionsPluginTests.m new file mode 100644 index 000000000000..89651b573822 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTQuickActionsPluginTests.m @@ -0,0 +1,210 @@ +// 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 Flutter; +@import quick_actions_ios; +@import XCTest; +#import + +@interface FLTQuickActionsPluginTests : XCTestCase + +@end + +@implementation FLTQuickActionsPluginTests + +- (void)testHandleMethodCall_setShortcutItems { + NSDictionary *rawItem = @{ + @"type" : @"SearchTheThing", + @"localizedTitle" : @"Search the thing", + @"icon" : @"search_the_thing.png", + }; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"setShortcutItems" + arguments:@[ rawItem ]]; + + FLTShortcutStateManager *mockShortcutStateManager = OCMClassMock([FLTShortcutStateManager class]); + + QuickActionsPlugin *plugin = + [[QuickActionsPlugin alloc] initWithChannel:OCMClassMock([FlutterMethodChannel class]) + shortcutStateManager:mockShortcutStateManager]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"result block must be called."]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertNil(result, @"result block must be called with nil."); + [resultExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + + OCMVerify([mockShortcutStateManager setShortcutItems:@[ rawItem ]]); +} + +- (void)testHandleMethodCall_clearShortcutItems { + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"clearShortcutItems" + arguments:nil]; + FLTShortcutStateManager *mockShortcutStateManager = OCMClassMock([FLTShortcutStateManager class]); + QuickActionsPlugin *plugin = + [[QuickActionsPlugin alloc] initWithChannel:OCMClassMock([FlutterMethodChannel class]) + shortcutStateManager:mockShortcutStateManager]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"result block must be called."]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertNil(result, @"result block must be called with nil."); + [resultExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + OCMVerify([mockShortcutStateManager setShortcutItems:@[]]); +} + +- (void)testHandleMethodCall_getLaunchAction { + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getLaunchAction" + arguments:nil]; + + QuickActionsPlugin *plugin = + [[QuickActionsPlugin alloc] initWithChannel:OCMClassMock([FlutterMethodChannel class]) + shortcutStateManager:OCMClassMock([FLTShortcutStateManager class])]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"result block must be called."]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertNil(result, @"result block must be called with nil."); + [resultExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testHandleMethodCall_nonExistMethods { + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"nonExist" arguments:nil]; + + QuickActionsPlugin *plugin = + [[QuickActionsPlugin alloc] initWithChannel:OCMClassMock([FlutterMethodChannel class]) + shortcutStateManager:OCMClassMock([FLTShortcutStateManager class])]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"result must be called."]; + [plugin + handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertEqual(result, FlutterMethodNotImplemented, + @"result block must be called with FlutterMethodNotImplemented"); + [resultExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testApplicationPerformActionForShortcutItem { + id mockChannel = OCMClassMock([FlutterMethodChannel class]); + QuickActionsPlugin *plugin = + [[QuickActionsPlugin alloc] initWithChannel:mockChannel + shortcutStateManager:OCMClassMock([FLTShortcutStateManager class])]; + + UIApplicationShortcutItem *item = [[UIApplicationShortcutItem alloc] + initWithType:@"SearchTheThing" + localizedTitle:@"Search the thing" + localizedSubtitle:nil + icon:[UIApplicationShortcutIcon + iconWithTemplateImageName:@"search_the_thing.png"] + userInfo:nil]; + + BOOL actionResult = [plugin application:[UIApplication sharedApplication] + performActionForShortcutItem:item + completionHandler:^(BOOL succeeded){/* no-op */}]; + XCTAssert(actionResult, @"performActionForShortcutItem must return true."); + OCMVerify([mockChannel invokeMethod:@"launch" arguments:item.type]); +} + +- (void)testApplicationDidFinishLaunchingWithOptions_launchWithShortcut { + id mockShortcutStateManager = OCMClassMock([FLTShortcutStateManager class]); + QuickActionsPlugin *plugin = + [[QuickActionsPlugin alloc] initWithChannel:OCMClassMock([FlutterMethodChannel class]) + shortcutStateManager:mockShortcutStateManager]; + + UIApplicationShortcutItem *item = [[UIApplicationShortcutItem alloc] + initWithType:@"SearchTheThing" + localizedTitle:@"Search the thing" + localizedSubtitle:nil + icon:[UIApplicationShortcutIcon + iconWithTemplateImageName:@"search_the_thing.png"] + userInfo:nil]; + + BOOL launchResult = [plugin application:[UIApplication sharedApplication] + didFinishLaunchingWithOptions:@{UIApplicationLaunchOptionsShortcutItemKey : item}]; + + XCTAssertFalse(launchResult, + @"didFinishLaunchingWithOptions must return false if launched from shortcut."); +} + +- (void)testApplicationDidFinishLaunchingWithOptions_launchWithoutShortcut { + QuickActionsPlugin *plugin = + [[QuickActionsPlugin alloc] initWithChannel:OCMClassMock([FlutterMethodChannel class]) + shortcutStateManager:OCMClassMock([FLTShortcutStateManager class])]; + BOOL launchResult = [plugin application:[UIApplication sharedApplication] + didFinishLaunchingWithOptions:@{}]; + XCTAssertTrue(launchResult, + @"didFinishLaunchingWithOptions must return true if not launched from shortcut."); +} + +- (void)testApplicationDidBecomeActive_launchWithoutShortcut { + id mockChannel = OCMClassMock([FlutterMethodChannel class]); + id mockShortcutStateManager = OCMClassMock([FLTShortcutStateManager class]); + QuickActionsPlugin *plugin = + [[QuickActionsPlugin alloc] initWithChannel:mockChannel + shortcutStateManager:mockShortcutStateManager]; + + BOOL launchResult = [plugin application:[UIApplication sharedApplication] + didFinishLaunchingWithOptions:@{}]; + XCTAssertTrue(launchResult, + @"didFinishLaunchingWithOptions must return true if not launched from shortcut."); + [plugin applicationDidBecomeActive:[UIApplication sharedApplication]]; + OCMVerify(never(), [mockChannel invokeMethod:OCMOCK_ANY arguments:OCMOCK_ANY]); +} + +- (void)testApplicationDidBecomeActive_launchWithShortcut { + id mockChannel = OCMClassMock([FlutterMethodChannel class]); + id mockShortcutStateManager = OCMClassMock([FLTShortcutStateManager class]); + QuickActionsPlugin *plugin = + [[QuickActionsPlugin alloc] initWithChannel:mockChannel + shortcutStateManager:mockShortcutStateManager]; + + UIApplicationShortcutItem *item = [[UIApplicationShortcutItem alloc] + initWithType:@"SearchTheThing" + localizedTitle:@"Search the thing" + localizedSubtitle:nil + icon:[UIApplicationShortcutIcon + iconWithTemplateImageName:@"search_the_thing.png"] + userInfo:nil]; + BOOL launchResult = [plugin application:[UIApplication sharedApplication] + didFinishLaunchingWithOptions:@{UIApplicationLaunchOptionsShortcutItemKey : item}]; + XCTAssertFalse(launchResult, + @"didFinishLaunchingWithOptions must return false if launched from shortcut."); + [plugin applicationDidBecomeActive:[UIApplication sharedApplication]]; + OCMVerify([mockChannel invokeMethod:@"launch" arguments:item.type]); +} + +- (void)testApplicationDidBecomeActive_launchWithShortcut_becomeActiveTwice { + id mockChannel = OCMClassMock([FlutterMethodChannel class]); + id mockShortcutStateManager = OCMClassMock([FLTShortcutStateManager class]); + QuickActionsPlugin *plugin = + [[QuickActionsPlugin alloc] initWithChannel:mockChannel + shortcutStateManager:mockShortcutStateManager]; + + UIApplicationShortcutItem *item = [[UIApplicationShortcutItem alloc] + initWithType:@"SearchTheThing" + localizedTitle:@"Search the thing" + localizedSubtitle:nil + icon:[UIApplicationShortcutIcon + iconWithTemplateImageName:@"search_the_thing.png"] + userInfo:nil]; + BOOL launchResult = [plugin application:[UIApplication sharedApplication] + didFinishLaunchingWithOptions:@{UIApplicationLaunchOptionsShortcutItemKey : item}]; + XCTAssertFalse(launchResult, + @"didFinishLaunchingWithOptions must return false if launched from shortcut."); + [plugin applicationDidBecomeActive:[UIApplication sharedApplication]]; + [plugin applicationDidBecomeActive:[UIApplication sharedApplication]]; + // shortcut should only be handled once per launch. + OCMVerify(times(1), [mockChannel invokeMethod:@"launch" arguments:item.type]); +} + +@end diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTShortcutStateManagerTests.m b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTShortcutStateManagerTests.m new file mode 100644 index 000000000000..96fbf229e566 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTShortcutStateManagerTests.m @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import quick_actions_ios; +@import XCTest; +#import + +@interface FLTShortcutStateManagerTests : XCTestCase +@end + +@implementation FLTShortcutStateManagerTests + +- (void)testSetShortcutItems_shouldSetItem { + id mockApplication = OCMPartialMock([UIApplication sharedApplication]); + OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); + + FLTShortcutStateManager *shortcutStateManager = [[FLTShortcutStateManager alloc] init]; + + NSDictionary *rawItem = @{ + @"type" : @"SearchTheThing", + @"localizedTitle" : @"Search the thing", + @"icon" : @"search_the_thing.png", + }; + + [shortcutStateManager setShortcutItems:@[ rawItem ]]; + + UIApplicationShortcutItem *expectedItem = [[UIApplicationShortcutItem alloc] + initWithType:@"SearchTheThing" + localizedTitle:@"Search the thing" + localizedSubtitle:nil + icon:[UIApplicationShortcutIcon + iconWithTemplateImageName:@"search_the_thing.png"] + userInfo:nil]; + + OCMVerify([mockApplication setShortcutItems:@[ expectedItem ]]); +} + +- (void)testSetShortcutItems_shouldSetItemWithoutIcon { + id mockApplication = OCMPartialMock([UIApplication sharedApplication]); + OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); + + NSDictionary *rawItem = @{ + @"type" : @"SearchTheThing", + @"localizedTitle" : @"Search the thing", + // Dart's null value is passed to iOS as `NSNull`. + // The key value pair is still present in the dictionary. + @"icon" : [NSNull null], + }; + FLTShortcutStateManager *shortcutStateManager = [[FLTShortcutStateManager alloc] init]; + [shortcutStateManager setShortcutItems:@[ rawItem ]]; + + UIApplicationShortcutItem *expectedItem = + [[UIApplicationShortcutItem alloc] initWithType:@"SearchTheThing" + localizedTitle:@"Search the thing" + localizedSubtitle:nil + icon:nil + userInfo:nil]; + OCMVerify([mockApplication setShortcutItems:@[ expectedItem ]]); +} + +@end diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/RunnerTests.m b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/RunnerTests.m deleted file mode 100644 index 4a96d05acb58..000000000000 --- a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/RunnerTests.m +++ /dev/null @@ -1,18 +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 quick_actions_ios; -@import XCTest; - -@interface QuickActionsTests : XCTestCase -@end - -@implementation QuickActionsTests - -- (void)testPlugin { - FLTQuickActionsPlugin *plugin = [[FLTQuickActionsPlugin alloc] init]; - XCTAssertNotNil(plugin); -} - -@end diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerUITests/RunnerUITests.m b/packages/quick_actions/quick_actions_ios/example/ios/RunnerUITests/RunnerUITests.m deleted file mode 100644 index 0bad57f886de..000000000000 --- a/packages/quick_actions/quick_actions_ios/example/ios/RunnerUITests/RunnerUITests.m +++ /dev/null @@ -1,99 +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 -#import - -static const int kElementWaitingTime = 30; - -@interface RunnerUITests : XCTestCase - -@end - -@implementation RunnerUITests { - XCUIApplication *_exampleApp; -} - -- (void)setUp { - [super setUp]; - self.continueAfterFailure = NO; - _exampleApp = [[XCUIApplication alloc] init]; -} - -- (void)tearDown { - [super tearDown]; - [_exampleApp terminate]; - _exampleApp = nil; -} - -- (void)testQuickActionWithFreshStart { - XCUIApplication *springboard = - [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"]; - XCUIElement *quickActionsAppIcon = springboard.icons[@"quick_actions_example"]; - if (![quickActionsAppIcon waitForExistenceWithTimeout:kElementWaitingTime]) { - os_log_error(OS_LOG_DEFAULT, "%@", springboard.debugDescription); - XCTFail(@"Failed due to not able to find the example app from springboard with %@ seconds", - @(kElementWaitingTime)); - } - - [quickActionsAppIcon pressForDuration:2]; - XCUIElement *actionTwo = springboard.buttons[@"Action two"]; - if (![actionTwo waitForExistenceWithTimeout:kElementWaitingTime]) { - os_log_error(OS_LOG_DEFAULT, "%@", springboard.debugDescription); - XCTFail(@"Failed due to not able to find the actionTwo button from springboard with %@ seconds", - @(kElementWaitingTime)); - } - - [actionTwo tap]; - - XCUIElement *actionTwoConfirmation = _exampleApp.otherElements[@"action_two"]; - if (![actionTwoConfirmation waitForExistenceWithTimeout:kElementWaitingTime]) { - os_log_error(OS_LOG_DEFAULT, "%@", springboard.debugDescription); - XCTFail(@"Failed due to not able to find the actionTwoConfirmation in the app with %@ seconds", - @(kElementWaitingTime)); - } - XCTAssertTrue(actionTwoConfirmation.exists); -} - -- (void)testQuickActionWhenAppIsInBackground { - [_exampleApp launch]; - - XCUIElement *actionsReady = _exampleApp.otherElements[@"actions ready"]; - if (![actionsReady waitForExistenceWithTimeout:kElementWaitingTime]) { - os_log_error(OS_LOG_DEFAULT, "%@", _exampleApp.debugDescription); - XCTFail(@"Failed due to not able to find the actionsReady in the app with %@ seconds", - @(kElementWaitingTime)); - } - - [[XCUIDevice sharedDevice] pressButton:XCUIDeviceButtonHome]; - - XCUIApplication *springboard = - [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"]; - XCUIElement *quickActionsAppIcon = springboard.icons[@"quick_actions_example"]; - if (![quickActionsAppIcon waitForExistenceWithTimeout:kElementWaitingTime]) { - os_log_error(OS_LOG_DEFAULT, "%@", springboard.debugDescription); - XCTFail(@"Failed due to not able to find the example app from springboard with %@ seconds", - @(kElementWaitingTime)); - } - - [quickActionsAppIcon pressForDuration:2]; - XCUIElement *actionOne = springboard.buttons[@"Action one"]; - if (![actionOne waitForExistenceWithTimeout:kElementWaitingTime]) { - os_log_error(OS_LOG_DEFAULT, "%@", springboard.debugDescription); - XCTFail(@"Failed due to not able to find the actionOne button from springboard with %@ seconds", - @(kElementWaitingTime)); - } - - [actionOne tap]; - - XCUIElement *actionOneConfirmation = _exampleApp.otherElements[@"action_one"]; - if (![actionOneConfirmation waitForExistenceWithTimeout:kElementWaitingTime]) { - os_log_error(OS_LOG_DEFAULT, "%@", springboard.debugDescription); - XCTFail(@"Failed due to not able to find the actionOneConfirmation in the app with %@ seconds", - @(kElementWaitingTime)); - } - XCTAssertTrue(actionOneConfirmation.exists); -} - -@end diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerUITests/RunnerUITests.swift b/packages/quick_actions/quick_actions_ios/example/ios/RunnerUITests/RunnerUITests.swift new file mode 100644 index 000000000000..a59692e7639d --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerUITests/RunnerUITests.swift @@ -0,0 +1,96 @@ +// 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 XCTest + +private let elementWaitingTime: TimeInterval = 30 + +class RunnerUITests: XCTestCase { + + private var exampleApp: XCUIApplication! + + override func setUp() { + super.setUp() + self.continueAfterFailure = false + exampleApp = XCUIApplication() + } + + override func tearDown() { + super.tearDown() + exampleApp.terminate() + exampleApp = nil + } + + func testQuickActionWithFreshStart() { + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + let quickActionsAppIcon = springboard.icons["quick_actions_example"] + if !quickActionsAppIcon.waitForExistence(timeout: elementWaitingTime) { + XCTFail( + "Failed due to not able to find the example app from springboard with \(elementWaitingTime) seconds. Springboard debug description: \(springboard.debugDescription)" + ) + } + + quickActionsAppIcon.press(forDuration: 2) + + let actionTwo = springboard.buttons["Action two"] + if !actionTwo.waitForExistence(timeout: elementWaitingTime) { + XCTFail( + "Failed due to not able to find the actionTwo button from springboard with \(elementWaitingTime) seconds. Springboard debug description: \(springboard.debugDescription)" + ) + } + + actionTwo.tap() + + let actionTwoConfirmation = exampleApp.otherElements["action_two"] + if !actionTwoConfirmation.waitForExistence(timeout: elementWaitingTime) { + XCTFail( + "Failed due to not able to find the actionTwoConfirmation in the app with \(elementWaitingTime) seconds. Springboard debug description: \(springboard.debugDescription)" + ) + } + + XCTAssert(actionTwoConfirmation.exists) + } + + func testQuickActionWhenAppIsInBackground() { + exampleApp.launch() + + let actionsReady = exampleApp.otherElements["actions ready"] + + if !actionsReady.waitForExistence(timeout: elementWaitingTime) { + XCTFail( + "Failed due to not able to find the actionsReady in the app with \(elementWaitingTime) seconds. App debug description: \(exampleApp.debugDescription)" + ) + } + + XCUIDevice.shared.press(.home) + + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + let quickActionsAppIcon = springboard.icons["quick_actions_example"] + if !quickActionsAppIcon.waitForExistence(timeout: elementWaitingTime) { + XCTFail( + "Failed due to not able to find the example app from springboard with \(elementWaitingTime) seconds. Springboard debug description: \(springboard.debugDescription)" + ) + } + + quickActionsAppIcon.press(forDuration: 2) + + let actionOne = springboard.buttons["Action one"] + if !actionOne.waitForExistence(timeout: elementWaitingTime) { + XCTFail( + "Failed due to not able to find the actionOne button from springboard with \(elementWaitingTime) seconds. Springboard debug description: \(springboard.debugDescription)" + ) + } + + actionOne.tap() + + let actionOneConfirmation = exampleApp.otherElements["action_one"] + if !actionOneConfirmation.waitForExistence(timeout: elementWaitingTime) { + XCTFail( + "Failed due to not able to find the actionOneConfirmation in the app with \(elementWaitingTime) seconds. Springboard debug description: \(springboard.debugDescription)" + ) + } + + XCTAssert(actionOneConfirmation.exists) + } +} diff --git a/packages/quick_actions/quick_actions_ios/example/lib/main.dart b/packages/quick_actions/quick_actions_ios/example/lib/main.dart index 5173d952d623..008917b724e0 100644 --- a/packages/quick_actions/quick_actions_ios/example/lib/main.dart +++ b/packages/quick_actions/quick_actions_ios/example/lib/main.dart @@ -8,10 +8,12 @@ import 'package:flutter/material.dart'; import 'package:quick_actions_ios/quick_actions_ios.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -28,7 +30,7 @@ class MyHomePage extends StatefulWidget { const MyHomePage({Key? key}) : super(key: key); @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { diff --git a/packages/quick_actions/quick_actions_ios/example/pubspec.yaml b/packages/quick_actions/quick_actions_ios/example/pubspec.yaml index 49c6b5e89ed4..ecac371720d6 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.8.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -20,6 +20,8 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/quick_actions/quick_actions_ios/ios/Classes/FLTQuickActionsPlugin.m b/packages/quick_actions/quick_actions_ios/ios/Classes/FLTQuickActionsPlugin.m deleted file mode 100644 index 883352c2ac1d..000000000000 --- a/packages/quick_actions/quick_actions_ios/ios/Classes/FLTQuickActionsPlugin.m +++ /dev/null @@ -1,110 +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 "FLTQuickActionsPlugin.h" - -static NSString *const kChannelName = @"plugins.flutter.io/quick_actions_ios"; - -@interface FLTQuickActionsPlugin () -@property(nonatomic, retain) FlutterMethodChannel *channel; -@property(nonatomic, retain) NSString *shortcutType; -@end - -@implementation FLTQuickActionsPlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:kChannelName - binaryMessenger:[registrar messenger]]; - FLTQuickActionsPlugin *instance = [[FLTQuickActionsPlugin alloc] init]; - instance.channel = channel; - [registrar addMethodCallDelegate:instance channel:channel]; - [registrar addApplicationDelegate:instance]; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([call.method isEqualToString:@"setShortcutItems"]) { - _setShortcutItems(call.arguments); - result(nil); - } else if ([call.method isEqualToString:@"clearShortcutItems"]) { - [UIApplication sharedApplication].shortcutItems = @[]; - result(nil); - } else if ([call.method isEqualToString:@"getLaunchAction"]) { - result(nil); - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)dealloc { - [_channel setMethodCallHandler:nil]; - _channel = nil; -} - -- (BOOL)application:(UIApplication *)application - performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem - completionHandler:(void (^)(BOOL succeeded))completionHandler - API_AVAILABLE(ios(9.0)) { - [self handleShortcut:shortcutItem.type]; - return YES; -} - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - UIApplicationShortcutItem *shortcutItem = - launchOptions[UIApplicationLaunchOptionsShortcutItemKey]; - if (shortcutItem) { - // Keep hold of the shortcut type and handle it in the - // `applicationDidBecomeActure:` method once the Dart MethodChannel - // is initialized. - self.shortcutType = shortcutItem.type; - - // Return NO to indicate we handled the quick action to ensure - // the `application:performActionFor:` method is not called (as - // per Apple's documentation: - // https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622935-application?language=objc). - return NO; - } - return YES; -} - -- (void)applicationDidBecomeActive:(UIApplication *)application { - if (self.shortcutType) { - [self handleShortcut:self.shortcutType]; - self.shortcutType = nil; - } -} - -#pragma mark Private functions - -- (void)handleShortcut:(NSString *)shortcut { - [self.channel invokeMethod:@"launch" arguments:shortcut]; -} - -NS_INLINE void _setShortcutItems(NSArray *items) API_AVAILABLE(ios(9.0)) { - NSMutableArray *newShortcuts = [[NSMutableArray alloc] init]; - - for (id item in items) { - UIApplicationShortcutItem *shortcut = _deserializeShortcutItem(item); - [newShortcuts addObject:shortcut]; - } - - [UIApplication sharedApplication].shortcutItems = newShortcuts; -} - -NS_INLINE UIApplicationShortcutItem *_deserializeShortcutItem(NSDictionary *serialized) - API_AVAILABLE(ios(9.0)) { - UIApplicationShortcutIcon *icon = - [serialized[@"icon"] isKindOfClass:[NSNull class]] - ? nil - : [UIApplicationShortcutIcon iconWithTemplateImageName:serialized[@"icon"]]; - - return [[UIApplicationShortcutItem alloc] initWithType:serialized[@"type"] - localizedTitle:serialized[@"localizedTitle"] - localizedSubtitle:nil - icon:icon - userInfo:nil]; -} - -@end diff --git a/packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.h b/packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.h new file mode 100644 index 000000000000..05d0433db6e0 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.h @@ -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 + +NS_ASSUME_NONNULL_BEGIN + +/// Manages the shortcut related states. +@interface FLTShortcutStateManager : NSObject + +/// Sets the list of shortcut items. +/// +/// @param items the list of shortcut items to be parsed and set. +- (void)setShortcutItems:(NSArray *)items API_AVAILABLE(ios(9.0)); +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.m b/packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.m new file mode 100644 index 000000000000..e39edd2b0f37 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.m @@ -0,0 +1,32 @@ +// 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 "FLTShortcutStateManager.h" + +@implementation FLTShortcutStateManager + +- (void)setShortcutItems:(NSArray *)items { + NSMutableArray *newShortcuts = [[NSMutableArray alloc] init]; + + for (id item in items) { + UIApplicationShortcutItem *shortcut = [self deserializeShortcutItem:item]; + [newShortcuts addObject:shortcut]; + } + + [UIApplication sharedApplication].shortcutItems = newShortcuts; +} + +- (UIApplicationShortcutItem *)deserializeShortcutItem:(NSDictionary *)serialized { + UIApplicationShortcutIcon *icon = + [serialized[@"icon"] isKindOfClass:[NSNull class]] + ? nil + : [UIApplicationShortcutIcon iconWithTemplateImageName:serialized[@"icon"]]; + return [[UIApplicationShortcutItem alloc] initWithType:serialized[@"type"] + localizedTitle:serialized[@"localizedTitle"] + localizedSubtitle:nil + icon:icon + userInfo:nil]; +} + +@end diff --git a/packages/quick_actions/quick_actions_ios/ios/Classes/QuickActionsPlugin.swift b/packages/quick_actions/quick_actions_ios/ios/Classes/QuickActionsPlugin.swift new file mode 100644 index 000000000000..26d6d20f8c02 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/ios/Classes/QuickActionsPlugin.swift @@ -0,0 +1,90 @@ +// 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 Flutter + +public final class QuickActionsPlugin: NSObject, FlutterPlugin { + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel( + name: "plugins.flutter.io/quick_actions_ios", + binaryMessenger: registrar.messenger()) + let instance = QuickActionsPlugin(channel: channel) + registrar.addMethodCallDelegate(instance, channel: channel) + registrar.addApplicationDelegate(instance) + } + + private let channel: FlutterMethodChannel + private let shortcutStateManager: FLTShortcutStateManager + /// The type of the shortcut item selected when launching the app. + private var launchingShortcutType: String? = nil + + // TODO: (hellohuanlin) remove `@objc` attribute and make it non-public after migrating tests to Swift. + @objc + public init( + channel: FlutterMethodChannel, + shortcutStateManager: FLTShortcutStateManager = FLTShortcutStateManager() + ) { + self.channel = channel + self.shortcutStateManager = shortcutStateManager + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "setShortcutItems": + // `arguments` must be an array of dictionaries + let items = call.arguments as! [[String: Any]] + shortcutStateManager.setShortcutItems(items) + result(nil) + case "clearShortcutItems": + shortcutStateManager.setShortcutItems([]) + result(nil) + case "getLaunchAction": + result(nil) + case _: + result(FlutterMethodNotImplemented) + } + } + + public func application( + _ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, + completionHandler: @escaping (Bool) -> Void + ) -> Bool { + handleShortcut(shortcutItem.type) + return true + } + + public func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [AnyHashable: Any] = [:] + ) -> Bool { + if let shortcutItem = launchOptions[UIApplication.LaunchOptionsKey.shortcutItem] + as? UIApplicationShortcutItem + { + // Keep hold of the shortcut type and handle it in the + // `applicationDidBecomeActive:` method once the Dart MethodChannel + // is initialized. + launchingShortcutType = shortcutItem.type + + // Return false to indicate we handled the quick action to ensure + // the `application:performActionFor:` method is not called (as + // per Apple's documentation: + // https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622935-application). + return false + } + return true + } + + public func applicationDidBecomeActive(_ application: UIApplication) { + if let shortcutType = launchingShortcutType { + handleShortcut(shortcutType) + launchingShortcutType = nil + } + } + + private func handleShortcut(_ shortcut: String) { + channel.invokeMethod("launch", arguments: shortcut) + } + +} diff --git a/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios.podspec b/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios.podspec index e8485f9b4436..d8090caa8ef6 100644 --- a/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios.podspec +++ b/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios.podspec @@ -14,7 +14,12 @@ Downloaded by pub (not CocoaPods). s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/quick_actions' } s.documentation_url = 'https://pub.dev/packages/quick_actions' - s.source_files = 'Classes/**/*' + s.swift_version = '5.0' + s.source_files = 'Classes/**/*.{h,m,swift}' + s.xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', + 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', + } s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' s.platform = :ios, '9.0' diff --git a/packages/quick_actions/quick_actions_ios/pubspec.yaml b/packages/quick_actions/quick_actions_ios/pubspec.yaml index 26644ba12fde..f01ae4aed9c3 100644 --- a/packages/quick_actions/quick_actions_ios/pubspec.yaml +++ b/packages/quick_actions/quick_actions_ios/pubspec.yaml @@ -2,18 +2,18 @@ name: quick_actions_ios description: An implementation for the iOS platform of the Flutter `quick_actions` plugin. repository: https://github.com/flutter/plugins/tree/main/packages/quick_actions/quick_actions_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.6.0+9 +version: 1.0.1 environment: sdk: ">=2.15.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.0" flutter: plugin: implements: quick_actions platforms: ios: - pluginClass: FLTQuickActionsPlugin + pluginClass: QuickActionsPlugin dartPluginClass: QuickActionsIos dependencies: @@ -27,4 +27,3 @@ dev_dependencies: integration_test: sdk: flutter plugin_platform_interface: ^2.1.2 - \ No newline at end of file diff --git a/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md b/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md index ad959de03be8..950864f96653 100644 --- a/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.0.3 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. + ## 1.0.2 * Removes dependency on `meta`. diff --git a/packages/quick_actions/quick_actions_platform_interface/lib/method_channel/method_channel_quick_actions.dart b/packages/quick_actions/quick_actions_platform_interface/lib/method_channel/method_channel_quick_actions.dart index 560c199ee77a..0f936db870c7 100644 --- a/packages/quick_actions/quick_actions_platform_interface/lib/method_channel/method_channel_quick_actions.dart +++ b/packages/quick_actions/quick_actions_platform_interface/lib/method_channel/method_channel_quick_actions.dart @@ -4,9 +4,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:quick_actions_platform_interface/types/types.dart'; import '../platform_interface/quick_actions_platform.dart'; +import '../types/types.dart'; const MethodChannel _channel = MethodChannel('plugins.flutter.io/quick_actions'); diff --git a/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.dart b/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.dart index 7a70bba5c81d..057cb5642293 100644 --- a/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.dart +++ b/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.dart @@ -3,9 +3,9 @@ // found in the LICENSE file. import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:quick_actions_platform_interface/types/types.dart'; import '../method_channel/method_channel_quick_actions.dart'; +import '../types/types.dart'; /// The interface that implementations of quick_actions must implement. /// diff --git a/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml b/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml index aef2e248bade..2990da603c14 100644 --- a/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml +++ b/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/quick_actions/ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+quick_actions%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: 1.0.2 +version: 1.0.3 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -19,4 +19,3 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.1 - pedantic: ^1.11.0 diff --git a/packages/quick_actions/quick_actions_platform_interface/test/quick_actions_platform_interface_test.dart b/packages/quick_actions/quick_actions_platform_interface/test/quick_actions_platform_interface_test.dart index b9655dc56a3c..ab3299b0cc14 100644 --- a/packages/quick_actions/quick_actions_platform_interface/test/quick_actions_platform_interface_test.dart +++ b/packages/quick_actions/quick_actions_platform_interface/test/quick_actions_platform_interface_test.dart @@ -21,7 +21,14 @@ void main() { test('Cannot be implemented with `implements`', () { expect(() { QuickActionsPlatform.instance = ImplementsQuickActionsPlatform(); - }, throwsNoSuchMethodError); + // In versions of `package:plugin_platform_interface` prior to fixing + // https://github.com/flutter/flutter/issues/109339, an attempt to + // implement a platform interface using `implements` would sometimes + // throw a `NoSuchMethodError` and other times throw an + // `AssertionError`. After the issue is fixed, an `AssertionError` will + // always be thrown. For the purpose of this test, we don't really care + // what exception is thrown, so just allow any exception. + }, throwsA(anything)); }); test('Can be extended', () { diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 36e60cc35763..b719ff158ff4 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,18 @@ +## NEXT + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. +* Updates minimum Flutter version to 2.10. + +## 2.0.15 + +* Minor fixes for new analysis options. + +## 2.0.14 + +* Adds OS version support information to README. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.0.13 * Updates documentation on README.md. diff --git a/packages/shared_preferences/shared_preferences/README.md b/packages/shared_preferences/shared_preferences/README.md index d3295ac6f6b9..03975ff021e6 100644 --- a/packages/shared_preferences/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -3,13 +3,17 @@ [![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dev/packages/shared_preferences) Wraps platform-specific persistent storage for simple data -(NSUserDefaults on iOS and macOS, SharedPreferences on Android, etc.). +(NSUserDefaults on iOS and macOS, SharedPreferences on Android, etc.). Data may be persisted to disk asynchronously, and there is no guarantee that writes will be persisted to disk after returning, so this plugin must not be used for storing critical data. Supported data types are `int`, `double`, `bool`, `String` and `List`. +| | Android | iOS | Linux | macOS | Web | Windows | +|-------------|---------|------|-------|--------|-----|-------------| +| **Support** | SDK 16+ | 9.0+ | Any | 10.11+ | Any | Any | + ## Usage To use this plugin, add `shared_preferences` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). @@ -17,24 +21,24 @@ To use this plugin, add `shared_preferences` as a [dependency in your pubspec.ya Here are small examples that show you how to use the API. #### Write data -```dart +```dart // Obtain shared preferences. final prefs = await SharedPreferences.getInstance(); -// Save an integer value to 'counter' key. +// Save an integer value to 'counter' key. await prefs.setInt('counter', 10); -// Save an boolean value to 'repeat' key. +// Save an boolean value to 'repeat' key. await prefs.setBool('repeat', true); -// Save an double value to 'decimal' key. +// Save an double value to 'decimal' key. await prefs.setDouble('decimal', 1.5); -// Save an String value to 'action' key. +// Save an String value to 'action' key. await prefs.setString('action', 'Start'); -// Save an list of strings to 'items' key. +// Save an list of strings to 'items' key. await prefs.setStringList('items', ['Earth', 'Moon', 'Sun']); ``` #### Read data -```dart +```dart // Try reading data from the 'counter' key. If it doesn't exist, returns null. final int? counter = prefs.getInt('counter'); // Try reading data from the 'repeat' key. If it doesn't exist, returns null. @@ -48,8 +52,8 @@ final List? items = prefs.getStringList('items'); ``` #### Remove an entry -```dart -// Remove data for the 'counter' key. +```dart +// Remove data for the 'counter' key. final success = await prefs.remove('counter'); ``` diff --git a/packages/shared_preferences/shared_preferences/example/README.md b/packages/shared_preferences/shared_preferences/example/README.md index 7dd9e9c4aa42..c060637c7ec5 100644 --- a/packages/shared_preferences/shared_preferences/example/README.md +++ b/packages/shared_preferences/shared_preferences/example/README.md @@ -1,8 +1,3 @@ # shared_preferences_example Demonstrates how to use the shared_preferences plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/shared_preferences/shared_preferences/example/lib/main.dart b/packages/shared_preferences/shared_preferences/example/lib/main.dart index 43f3c78ad920..a2e72b446925 100644 --- a/packages/shared_preferences/shared_preferences/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences/example/lib/main.dart @@ -10,10 +10,12 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return const MaterialApp( diff --git a/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugins.cmake b/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugins.cmake index 51436ae8c982..2e1de87a7eb6 100644 --- a/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugins.cmake +++ b/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -13,3 +16,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/shared_preferences/shared_preferences/example/pubspec.yaml b/packages/shared_preferences/shared_preferences/example/pubspec.yaml index 1cb0f185baf4..6964656d16ef 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.5.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -20,9 +20,10 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugins.cmake b/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugins.cmake index 4d10c2518654..b93c4c30c167 100644 --- a/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugins.cmake +++ b/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -13,3 +16,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index 5e2a65889bee..77f04800a5bb 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -140,7 +140,7 @@ class SharedPreferences { /// Always returns true. /// On iOS, synchronize is marked deprecated. On Android, we commit every set. - @deprecated + @Deprecated('This method is now a no-op, and should no longer be called.') Future commit() async => true; /// Completes with true once the user preferences for the app has been cleared. diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index 39b48ef51ed2..7a310d9a584a 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.13 +version: 2.0.15 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: @@ -43,4 +43,3 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart index 11498cfa5dcb..30f7829f670a 100755 --- a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart @@ -171,22 +171,22 @@ void main() { }); group('mocking', () { - const String _key = 'dummy'; - const String _prefixedKey = 'flutter.' + _key; + const String key = 'dummy'; + const String prefixedKey = 'flutter.$key'; test('test 1', () async { SharedPreferences.setMockInitialValues( - {_prefixedKey: 'my string'}); + {prefixedKey: 'my string'}); final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String? value = prefs.getString(_key); + final String? value = prefs.getString(key); expect(value, 'my string'); }); test('test 2', () async { SharedPreferences.setMockInitialValues( - {_prefixedKey: 'my other string'}); + {prefixedKey: 'my other string'}); final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String? value = prefs.getString(_key); + final String? value = prefs.getString(key); expect(value, 'my other string'); }); }); diff --git a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md index 5321e869c497..d9d7bbd82f46 100644 --- a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md @@ -1,3 +1,18 @@ +## 2.0.14 + +* Fixes typo in `SharedPreferencesAndroid` docs. +* Updates code for `no_leading_underscores_for_local_identifiers` lint. + +## 2.0.13 + +* Updates gradle to 7.2.2. +* Updates minimum Flutter version to 2.10. + +## 2.0.12 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.0.11 * Switches to an in-package method channel implementation. diff --git a/packages/shared_preferences/shared_preferences_android/android/build.gradle b/packages/shared_preferences/shared_preferences_android/android/build.gradle index 6eeab718059f..feae770a475d 100644 --- a/packages/shared_preferences/shared_preferences_android/android/build.gradle +++ b/packages/shared_preferences/shared_preferences_android/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' + classpath 'com.android.tools.build:gradle:7.2.2' } } @@ -37,13 +37,14 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { + disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' baseline file("lint-baseline.xml") } dependencies { - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:3.9.0' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-inline:4.7.0' } diff --git a/packages/shared_preferences/shared_preferences_android/example/README.md b/packages/shared_preferences/shared_preferences_android/example/README.md index 7dd9e9c4aa42..96b8bb17dbff 100644 --- a/packages/shared_preferences/shared_preferences_android/example/README.md +++ b/packages/shared_preferences/shared_preferences_android/example/README.md @@ -1,8 +1,9 @@ -# shared_preferences_example +# Platform Implementation Test App -Demonstrates how to use the shared_preferences plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart index 275067d98770..4d4a85a5fcbc 100644 --- a/packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart @@ -39,42 +39,42 @@ void main() { // Normally the app-facing package adds the prefix, but since this test // bypasses the app-facing package it needs to be manually added. - String _prefixedKey(String key) { + String prefixedKey(String key) { return 'flutter.$key'; } testWidgets('reading', (WidgetTester _) async { final Map values = await preferences.getAll(); - expect(values[_prefixedKey('String')], isNull); - expect(values[_prefixedKey('bool')], isNull); - expect(values[_prefixedKey('int')], isNull); - expect(values[_prefixedKey('double')], isNull); - expect(values[_prefixedKey('List')], isNull); + expect(values[prefixedKey('String')], isNull); + expect(values[prefixedKey('bool')], isNull); + expect(values[prefixedKey('int')], isNull); + expect(values[prefixedKey('double')], isNull); + expect(values[prefixedKey('List')], isNull); }); testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ preferences.setValue( - 'String', _prefixedKey('String'), kTestValues2['flutter.String']!), + 'String', prefixedKey('String'), kTestValues2['flutter.String']!), preferences.setValue( - 'Bool', _prefixedKey('bool'), kTestValues2['flutter.bool']!), + 'Bool', prefixedKey('bool'), kTestValues2['flutter.bool']!), preferences.setValue( - 'Int', _prefixedKey('int'), kTestValues2['flutter.int']!), + 'Int', prefixedKey('int'), kTestValues2['flutter.int']!), preferences.setValue( - 'Double', _prefixedKey('double'), kTestValues2['flutter.double']!), + 'Double', prefixedKey('double'), kTestValues2['flutter.double']!), preferences.setValue( - 'StringList', _prefixedKey('List'), kTestValues2['flutter.List']!) + 'StringList', prefixedKey('List'), kTestValues2['flutter.List']!) ]); final Map values = await preferences.getAll(); - expect(values[_prefixedKey('String')], kTestValues2['flutter.String']); - expect(values[_prefixedKey('bool')], kTestValues2['flutter.bool']); - expect(values[_prefixedKey('int')], kTestValues2['flutter.int']); - expect(values[_prefixedKey('double')], kTestValues2['flutter.double']); - expect(values[_prefixedKey('List')], kTestValues2['flutter.List']); + expect(values[prefixedKey('String')], kTestValues2['flutter.String']); + expect(values[prefixedKey('bool')], kTestValues2['flutter.bool']); + expect(values[prefixedKey('int')], kTestValues2['flutter.int']); + expect(values[prefixedKey('double')], kTestValues2['flutter.double']); + expect(values[prefixedKey('List')], kTestValues2['flutter.List']); }); testWidgets('removing', (WidgetTester _) async { - final String key = _prefixedKey('testKey'); + final String key = prefixedKey('testKey'); await preferences.setValue('String', key, kTestValues['flutter.String']!); await preferences.setValue('Bool', key, kTestValues['flutter.bool']!); await preferences.setValue('Int', key, kTestValues['flutter.int']!); @@ -108,19 +108,19 @@ void main() { final List> writes = >[]; const int writeCount = 100; for (int i = 1; i <= writeCount; i++) { - writes.add(preferences.setValue('Int', _prefixedKey('int'), i)); + writes.add(preferences.setValue('Int', prefixedKey('int'), i)); } final List result = await Future.wait(writes, eagerError: true); // All writes should succeed. expect(result.where((bool element) => !element), isEmpty); // The last write should win. final Map values = await preferences.getAll(); - expect(values[_prefixedKey('int')], writeCount); + expect(values[prefixedKey('int')], writeCount); }); testWidgets('string clash with lists, big integers and doubles', (WidgetTester _) async { - final String key = _prefixedKey('akey'); + final String key = prefixedKey('akey'); const String value = 'a string value'; await preferences.clear(); 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 06ee9434ba84..bb513b09f6d5 100644 --- a/packages/shared_preferences/shared_preferences_android/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_android/example/lib/main.dart @@ -8,10 +8,12 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return const MaterialApp( diff --git a/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml index 060b94b3ae82..bd1272c71d80 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.5.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -21,9 +21,10 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart b/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart index 86f447b8959c..da5147d32da2 100644 --- a/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart +++ b/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart @@ -10,7 +10,7 @@ import 'package:shared_preferences_platform_interface/shared_preferences_platfor const MethodChannel _kChannel = MethodChannel('plugins.flutter.io/shared_preferences_android'); -/// The macOS implementation of [SharedPreferencesStorePlatform]. +/// The Android implementation of [SharedPreferencesStorePlatform]. /// /// This class implements the `package:shared_preferences` functionality for Android. class SharedPreferencesAndroid extends SharedPreferencesStorePlatform { diff --git a/packages/shared_preferences/shared_preferences_android/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/pubspec.yaml index 7eb180f3ab48..7692c114bfce 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.11 +version: 2.0.14 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md b/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md index a5cc1d34e034..d2101e0784cf 100644 --- a/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md @@ -1,3 +1,13 @@ +## NEXT + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. +* Updates minimum Flutter version to 2.10. + +## 2.1.1 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.1.0 * Upgrades to using Pigeon. diff --git a/packages/shared_preferences/shared_preferences_ios/example/README.md b/packages/shared_preferences/shared_preferences_ios/example/README.md index 7dd9e9c4aa42..96b8bb17dbff 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/README.md +++ b/packages/shared_preferences/shared_preferences_ios/example/README.md @@ -1,8 +1,9 @@ -# shared_preferences_example +# Platform Implementation Test App -Demonstrates how to use the shared_preferences plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/shared_preferences/shared_preferences_ios/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_ios/example/integration_test/shared_preferences_test.dart index 3a6bab55eced..b4b21871701c 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_ios/example/integration_test/shared_preferences_test.dart @@ -38,42 +38,42 @@ void main() { // Normally the app-facing package adds the prefix, but since this test // bypasses the app-facing package it needs to be manually added. - String _prefixedKey(String key) { + String prefixedKey(String key) { return 'flutter.$key'; } testWidgets('reading', (WidgetTester _) async { final Map values = await preferences.getAll(); - expect(values[_prefixedKey('String')], isNull); - expect(values[_prefixedKey('bool')], isNull); - expect(values[_prefixedKey('int')], isNull); - expect(values[_prefixedKey('double')], isNull); - expect(values[_prefixedKey('List')], isNull); + expect(values[prefixedKey('String')], isNull); + expect(values[prefixedKey('bool')], isNull); + expect(values[prefixedKey('int')], isNull); + expect(values[prefixedKey('double')], isNull); + expect(values[prefixedKey('List')], isNull); }); testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ preferences.setValue( - 'String', _prefixedKey('String'), kTestValues2['flutter.String']!), + 'String', prefixedKey('String'), kTestValues2['flutter.String']!), preferences.setValue( - 'Bool', _prefixedKey('bool'), kTestValues2['flutter.bool']!), + 'Bool', prefixedKey('bool'), kTestValues2['flutter.bool']!), preferences.setValue( - 'Int', _prefixedKey('int'), kTestValues2['flutter.int']!), + 'Int', prefixedKey('int'), kTestValues2['flutter.int']!), preferences.setValue( - 'Double', _prefixedKey('double'), kTestValues2['flutter.double']!), + 'Double', prefixedKey('double'), kTestValues2['flutter.double']!), preferences.setValue( - 'StringList', _prefixedKey('List'), kTestValues2['flutter.List']!) + 'StringList', prefixedKey('List'), kTestValues2['flutter.List']!) ]); final Map values = await preferences.getAll(); - expect(values[_prefixedKey('String')], kTestValues2['flutter.String']); - expect(values[_prefixedKey('bool')], kTestValues2['flutter.bool']); - expect(values[_prefixedKey('int')], kTestValues2['flutter.int']); - expect(values[_prefixedKey('double')], kTestValues2['flutter.double']); - expect(values[_prefixedKey('List')], kTestValues2['flutter.List']); + expect(values[prefixedKey('String')], kTestValues2['flutter.String']); + expect(values[prefixedKey('bool')], kTestValues2['flutter.bool']); + expect(values[prefixedKey('int')], kTestValues2['flutter.int']); + expect(values[prefixedKey('double')], kTestValues2['flutter.double']); + expect(values[prefixedKey('List')], kTestValues2['flutter.List']); }); testWidgets('removing', (WidgetTester _) async { - final String key = _prefixedKey('testKey'); + final String key = prefixedKey('testKey'); await preferences.setValue('String', key, kTestValues['flutter.String']!); await preferences.setValue('Bool', key, kTestValues['flutter.bool']!); await preferences.setValue('Int', key, kTestValues['flutter.int']!); diff --git a/packages/shared_preferences/shared_preferences_ios/example/lib/main.dart b/packages/shared_preferences/shared_preferences_ios/example/lib/main.dart index 06ee9434ba84..bb513b09f6d5 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_ios/example/lib/main.dart @@ -8,10 +8,12 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return const MaterialApp( diff --git a/packages/shared_preferences/shared_preferences_ios/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_ios/example/pubspec.yaml index 4a00e6d23c0a..446cea1e0508 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_ios/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -21,9 +21,10 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/shared_preferences/shared_preferences_ios/pubspec.yaml b/packages/shared_preferences/shared_preferences_ios/pubspec.yaml index 33bf5baffd18..45b1aae2c473 100644 --- a/packages/shared_preferences/shared_preferences_ios/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: shared_preferences_ios description: iOS implementation of the shared_preferences plugin repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.1.0 +version: 2.1.1 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index 7c86b3c80dc8..5d59660b4d6b 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,3 +1,14 @@ +## NEXT + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. +* Updates minimum Flutter version to 2.10. + +## 2.1.1 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.1.0 * Deprecated `SharedPreferencesWindows.instance` in favor of `SharedPreferencesStorePlatform.instance`. diff --git a/packages/shared_preferences/shared_preferences_linux/example/README.md b/packages/shared_preferences/shared_preferences_linux/example/README.md index 7dd9e9c4aa42..96b8bb17dbff 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/README.md +++ b/packages/shared_preferences/shared_preferences_linux/example/README.md @@ -1,8 +1,9 @@ -# shared_preferences_example +# Platform Implementation Test App -Demonstrates how to use the shared_preferences plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart index 1d83eead9f25..664048ab98e4 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart @@ -2,8 +2,6 @@ // 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_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:shared_preferences_linux/shared_preferences_linux.dart'; 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 4b71c7ea3beb..d51be33baeed 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart @@ -10,10 +10,12 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences_linux/shared_preferences_linux.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return const MaterialApp( diff --git a/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugins.cmake b/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugins.cmake index 51436ae8c982..2e1de87a7eb6 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugins.cmake +++ b/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -13,3 +16,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml index d34973b9dde6..9418c0581ed7 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: ">=1.20.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -20,9 +20,10 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml index 8ab692a613e2..88889dcde5fd 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.0 +version: 2.1.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart b/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart index 57acd17f5094..176d1d9f9ead 100644 --- a/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart +++ b/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart @@ -20,22 +20,22 @@ void main() { pathProvider = FakePathProviderLinux(); }); - Future _getFilePath() async { + Future getFilePath() async { final String? directory = await pathProvider.getApplicationSupportPath(); return path.join(directory!, 'shared_preferences.json'); } - Future _writeTestFile(String value) async { - fs.file(await _getFilePath()) + Future writeTestFile(String value) async { + fs.file(await getFilePath()) ..createSync(recursive: true) ..writeAsStringSync(value); } - Future _readTestFile() async { - return fs.file(await _getFilePath()).readAsStringSync(); + Future readTestFile() async { + return fs.file(await getFilePath()).readAsStringSync(); } - SharedPreferencesLinux _getPreferences() { + SharedPreferencesLinux getPreferences() { final SharedPreferencesLinux prefs = SharedPreferencesLinux(); prefs.fs = fs; prefs.pathProvider = pathProvider; @@ -49,8 +49,8 @@ void main() { }); test('getAll', () async { - await _writeTestFile('{"key1": "one", "key2": 2}'); - final SharedPreferencesLinux prefs = _getPreferences(); + await writeTestFile('{"key1": "one", "key2": 2}'); + final SharedPreferencesLinux prefs = getPreferences(); final Map values = await prefs.getAll(); expect(values, hasLength(2)); @@ -59,30 +59,30 @@ void main() { }); test('remove', () async { - await _writeTestFile('{"key1":"one","key2":2}'); - final SharedPreferencesLinux prefs = _getPreferences(); + await writeTestFile('{"key1":"one","key2":2}'); + final SharedPreferencesLinux prefs = getPreferences(); await prefs.remove('key2'); - expect(await _readTestFile(), '{"key1":"one"}'); + expect(await readTestFile(), '{"key1":"one"}'); }); test('setValue', () async { - await _writeTestFile('{}'); - final SharedPreferencesLinux prefs = _getPreferences(); + await writeTestFile('{}'); + final SharedPreferencesLinux prefs = getPreferences(); await prefs.setValue('', 'key1', 'one'); await prefs.setValue('', 'key2', 2); - expect(await _readTestFile(), '{"key1":"one","key2":2}'); + expect(await readTestFile(), '{"key1":"one","key2":2}'); }); test('clear', () async { - await _writeTestFile('{"key1":"one","key2":2}'); - final SharedPreferencesLinux prefs = _getPreferences(); + await writeTestFile('{"key1":"one","key2":2}'); + final SharedPreferencesLinux prefs = getPreferences(); await prefs.clear(); - expect(await _readTestFile(), '{}'); + expect(await readTestFile(), '{}'); }); } diff --git a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md index 1f586a2a9581..fc8a78af95b9 100644 --- a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md @@ -1,3 +1,14 @@ +## NEXT + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. +* Updates minimum Flutter version to 2.10. + +## 2.0.4 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.0.3 * Switches to an in-package method channel implementation. diff --git a/packages/shared_preferences/shared_preferences_macos/example/README.md b/packages/shared_preferences/shared_preferences_macos/example/README.md index 7dd9e9c4aa42..96b8bb17dbff 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/README.md +++ b/packages/shared_preferences/shared_preferences_macos/example/README.md @@ -1,8 +1,9 @@ -# shared_preferences_example +# Platform Implementation Test App -Demonstrates how to use the shared_preferences plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart index 66e3be30ee5d..a980eab26679 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart @@ -2,8 +2,6 @@ // 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_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; @@ -40,42 +38,42 @@ void main() { // Normally the app-facing package adds the prefix, but since this test // bypasses the app-facing package it needs to be manually added. - String _prefixedKey(String key) { + String prefixedKey(String key) { return 'flutter.$key'; } testWidgets('reading', (WidgetTester _) async { final Map values = await preferences.getAll(); - expect(values[_prefixedKey('String')], isNull); - expect(values[_prefixedKey('bool')], isNull); - expect(values[_prefixedKey('int')], isNull); - expect(values[_prefixedKey('double')], isNull); - expect(values[_prefixedKey('List')], isNull); + expect(values[prefixedKey('String')], isNull); + expect(values[prefixedKey('bool')], isNull); + expect(values[prefixedKey('int')], isNull); + expect(values[prefixedKey('double')], isNull); + expect(values[prefixedKey('List')], isNull); }); testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ preferences.setValue( - 'String', _prefixedKey('String'), kTestValues2['flutter.String']!), + 'String', prefixedKey('String'), kTestValues2['flutter.String']!), preferences.setValue( - 'Bool', _prefixedKey('bool'), kTestValues2['flutter.bool']!), + 'Bool', prefixedKey('bool'), kTestValues2['flutter.bool']!), preferences.setValue( - 'Int', _prefixedKey('int'), kTestValues2['flutter.int']!), + 'Int', prefixedKey('int'), kTestValues2['flutter.int']!), preferences.setValue( - 'Double', _prefixedKey('double'), kTestValues2['flutter.double']!), + 'Double', prefixedKey('double'), kTestValues2['flutter.double']!), preferences.setValue( - 'StringList', _prefixedKey('List'), kTestValues2['flutter.List']!) + 'StringList', prefixedKey('List'), kTestValues2['flutter.List']!) ]); final Map values = await preferences.getAll(); - expect(values[_prefixedKey('String')], kTestValues2['flutter.String']); - expect(values[_prefixedKey('bool')], kTestValues2['flutter.bool']); - expect(values[_prefixedKey('int')], kTestValues2['flutter.int']); - expect(values[_prefixedKey('double')], kTestValues2['flutter.double']); - expect(values[_prefixedKey('List')], kTestValues2['flutter.List']); + expect(values[prefixedKey('String')], kTestValues2['flutter.String']); + expect(values[prefixedKey('bool')], kTestValues2['flutter.bool']); + expect(values[prefixedKey('int')], kTestValues2['flutter.int']); + expect(values[prefixedKey('double')], kTestValues2['flutter.double']); + expect(values[prefixedKey('List')], kTestValues2['flutter.List']); }); testWidgets('removing', (WidgetTester _) async { - final String key = _prefixedKey('testKey'); + final String key = prefixedKey('testKey'); await preferences.setValue('String', key, kTestValues['flutter.String']!); await preferences.setValue('Bool', key, kTestValues['flutter.bool']!); await preferences.setValue('Int', key, kTestValues['flutter.int']!); diff --git a/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart b/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart index 349e9c45405a..e6bbe5931471 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart @@ -10,10 +10,12 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return const MaterialApp( diff --git a/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml index d6f07f8eb2af..f650fb7a6268 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.8" + flutter: ">=2.10.0" dependencies: flutter: @@ -21,9 +21,10 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/shared_preferences/shared_preferences_macos/macos/shared_preferences_macos.podspec b/packages/shared_preferences/shared_preferences_macos/macos/shared_preferences_macos.podspec index df140fbb1eec..590b0c34adcf 100644 --- a/packages/shared_preferences/shared_preferences_macos/macos/shared_preferences_macos.podspec +++ b/packages/shared_preferences/shared_preferences_macos/macos/shared_preferences_macos.podspec @@ -11,7 +11,7 @@ Wraps NSUserDefaults, providing a persistent store for simple key-value pairs. s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_macos' s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_macos' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_macos' } s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' diff --git a/packages/shared_preferences/shared_preferences_macos/pubspec.yaml b/packages/shared_preferences/shared_preferences_macos/pubspec.yaml index 0873696e6d4f..77f5f11f0525 100644 --- a/packages/shared_preferences/shared_preferences_macos/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_macos/pubspec.yaml @@ -2,11 +2,11 @@ name: shared_preferences_macos description: macOS implementation of the shared_preferences plugin. repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.3 +version: 2.0.4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.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 51b59651b49a..3ef89c396222 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md @@ -1,6 +1,11 @@ ## NEXT -* Fixes newly enabled analyzer options. +* Updates minimum Flutter version to 2.10. + +## 2.1.0 + +* Adopts `plugin_platform_interface`. As a result, `isMock` is deprecated in + favor of the now-standard `MockPlatformInterfaceMixin`. ## 2.0.0 diff --git a/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart b/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart index 8023c864a399..ced6aa5c389f 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart +++ b/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'method_channel_shared_preferences.dart'; @@ -15,7 +16,12 @@ import 'method_channel_shared_preferences.dart'; /// (using `extends`) ensures that the subclass will get the default implementation, while /// platform implementations that `implements` this interface will be broken by newly added /// [SharedPreferencesStorePlatform] methods. -abstract class SharedPreferencesStorePlatform { +abstract class SharedPreferencesStorePlatform extends PlatformInterface { + /// Constructs a SharedPreferencesStorePlatform. + SharedPreferencesStorePlatform() : super(token: _token); + + static final Object _token = Object(); + /// The default instance of [SharedPreferencesStorePlatform] to use. /// /// Defaults to [MethodChannelSharedPreferencesStore]. @@ -23,16 +29,11 @@ abstract class SharedPreferencesStorePlatform { /// Platform-specific plugins should set this with their own platform-specific /// class that extends [SharedPreferencesStorePlatform] when they register themselves. - static set instance(SharedPreferencesStorePlatform value) { - if (!value.isMock) { - try { - value._verifyProvidesDefaultImplementations(); - } on NoSuchMethodError catch (_) { - throw AssertionError( - 'Platform interfaces must not be implemented with `implements`'); - } + static set instance(SharedPreferencesStorePlatform instance) { + if (!instance.isMock) { + PlatformInterface.verify(instance, _token); } - _instance = value; + _instance = instance; } static SharedPreferencesStorePlatform _instance = @@ -44,6 +45,7 @@ abstract class SharedPreferencesStorePlatform { /// other than mocks (see class docs). This property provides a backdoor for mockito mocks to /// skip the verification that the class isn't implemented with `implements`. @visibleForTesting + @Deprecated('Use MockPlatformInterfaceMixin instead') bool get isMock => false; /// Removes the value associated with the [key]. @@ -65,14 +67,6 @@ abstract class SharedPreferencesStorePlatform { /// Returns all key/value pairs persisted in this store. Future> getAll(); - - // This method makes sure that SharedPreferencesStorePlatform isn't implemented with `implements`. - // - // See class doc for more details on why implementing this class is forbidden. - // - // This private method is called by the instance setter, which fails if the class is - // implemented with `implements`. - void _verifyProvidesDefaultImplementations() {} } /// Stores data in memory. diff --git a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml index 8d775ab8b58c..b55eb1ccceb2 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml @@ -2,17 +2,17 @@ name: shared_preferences_platform_interface description: A common platform interface for the shared_preferences plugin. repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_platform_interface issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.0 +version: 2.1.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" dependencies: flutter: sdk: flutter + plugin_platform_interface: ^2.1.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences_platform_interface/test/shared_preferences_platform_interface_test.dart b/packages/shared_preferences/shared_preferences_platform_interface/test/shared_preferences_platform_interface_test.dart index 8efe885c122c..ed078e87909e 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/test/shared_preferences_platform_interface_test.dart +++ b/packages/shared_preferences/shared_preferences_platform_interface/test/shared_preferences_platform_interface_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; void main() { @@ -10,16 +11,30 @@ void main() { group(SharedPreferencesStorePlatform, () { test('disallows implementing interface', () { - expect( - () { - SharedPreferencesStorePlatform.instance = IllegalImplementation(); - }, - throwsAssertionError, - ); + expect(() { + SharedPreferencesStorePlatform.instance = IllegalImplementation(); + }, + // In versions of `package:plugin_platform_interface` prior to fixing + // https://github.com/flutter/flutter/issues/109339, an attempt to + // implement a platform interface using `implements` would sometimes + // throw a `NoSuchMethodError` and other times throw an + // `AssertionError`. After the issue is fixed, an `AssertionError` will + // always be thrown. For the purpose of this test, we don't really care + // what exception is thrown, so just allow any exception. + throwsA(anything)); + }); + + test('supports MockPlatformInterfaceMixin', () { + SharedPreferencesStorePlatform.instance = ModernMockImplementation(); + }); + + test('still supports legacy isMock', () { + SharedPreferencesStorePlatform.instance = LegacyIsMockImplementation(); }); }); } +/// An implementation using `implements` that isn't a mock, which isn't allowed. class IllegalImplementation implements SharedPreferencesStorePlatform { // Intentionally declare self as not a mock to trigger the // compliance check. @@ -46,3 +61,55 @@ class IllegalImplementation implements SharedPreferencesStorePlatform { throw UnimplementedError(); } } + +class LegacyIsMockImplementation implements SharedPreferencesStorePlatform { + @override + bool get isMock => true; + + @override + Future clear() { + throw UnimplementedError(); + } + + @override + Future> getAll() { + throw UnimplementedError(); + } + + @override + Future remove(String key) { + throw UnimplementedError(); + } + + @override + Future setValue(String valueType, String key, Object value) { + throw UnimplementedError(); + } +} + +class ModernMockImplementation + with MockPlatformInterfaceMixin + implements SharedPreferencesStorePlatform { + @override + bool get isMock => false; + + @override + Future clear() { + throw UnimplementedError(); + } + + @override + Future> getAll() { + throw UnimplementedError(); + } + + @override + Future remove(String key) { + throw UnimplementedError(); + } + + @override + Future setValue(String valueType, String key, Object value) { + throw UnimplementedError(); + } +} diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index 2a6ffa20e37b..8c8411da6fff 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 2.0.4 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.0.3 * Fixes newly enabled analyzer options. diff --git a/packages/shared_preferences/shared_preferences_web/example/README.md b/packages/shared_preferences/shared_preferences_web/example/README.md index 4348451b14e2..0e51ae5ecbd2 100644 --- a/packages/shared_preferences/shared_preferences_web/example/README.md +++ b/packages/shared_preferences/shared_preferences_web/example/README.md @@ -1,4 +1,14 @@ -# Testing +# 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. + +## Testing This package uses `package:integration_test` to run its tests in a web browser. diff --git a/packages/shared_preferences/shared_preferences_web/example/lib/main.dart b/packages/shared_preferences/shared_preferences_web/example/lib/main.dart index 341913a18490..87422953de6a 100644 --- a/packages/shared_preferences/shared_preferences_web/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_web/example/lib/main.dart @@ -5,13 +5,16 @@ import 'package:flutter/material.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } /// App for testing class MyApp extends StatefulWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { diff --git a/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml index 832ba912e5a8..050275489efa 100644 --- a/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml @@ -3,11 +3,12 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.2.0" + flutter: ">=2.10.0" dependencies: flutter: sdk: flutter + shared_preferences_platform_interface: ^2.0.0 shared_preferences_web: path: ../ diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml index 232c87c426fc..b64f37d10da6 100644 --- a/packages/shared_preferences/shared_preferences_web/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml @@ -2,11 +2,11 @@ name: shared_preferences_web description: Web platform implementation of shared_preferences repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.3 +version: 2.0.4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: @@ -26,4 +26,3 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md index 6c96681ce5b7..935a5ee54c2b 100644 --- a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md @@ -1,3 +1,13 @@ +## NEXT + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. +* Updates minimum Flutter version to 2.10. + +## 2.1.1 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.1.0 * Deprecated `SharedPreferencesWindows.instance` in favor of `SharedPreferencesStorePlatform.instance`. diff --git a/packages/shared_preferences/shared_preferences_windows/example/README.md b/packages/shared_preferences/shared_preferences_windows/example/README.md index d85bb4107622..96b8bb17dbff 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/README.md +++ b/packages/shared_preferences/shared_preferences_windows/example/README.md @@ -1,16 +1,9 @@ -# shared_preferences_windows_example +# Platform Implementation Test App -Demonstrates how to use the shared_preferences_windows plugin. +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. -## 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://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. 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 40a9159cee70..74d5e4c68772 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart @@ -10,10 +10,12 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences_windows/shared_preferences_windows.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return const MaterialApp( diff --git a/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml index 96762e933a9d..43c2145b32ae 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: ">=1.20.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -20,9 +20,10 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugins.cmake b/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugins.cmake index 4d10c2518654..b93c4c30c167 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugins.cmake +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -13,3 +16,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml index 6dcb5997f131..032e0fb213df 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.0 +version: 2.1.1 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart b/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart index 0c47e9865765..04fa335b703e 100644 --- a/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart +++ b/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart @@ -19,22 +19,22 @@ void main() { pathProvider = FakePathProviderWindows(); }); - Future _getFilePath() async { + Future getFilePath() async { final String? directory = await pathProvider.getApplicationSupportPath(); return path.join(directory!, 'shared_preferences.json'); } - Future _writeTestFile(String value) async { - fileSystem.file(await _getFilePath()) + Future writeTestFile(String value) async { + fileSystem.file(await getFilePath()) ..createSync(recursive: true) ..writeAsStringSync(value); } - Future _readTestFile() async { - return fileSystem.file(await _getFilePath()).readAsStringSync(); + Future readTestFile() async { + return fileSystem.file(await getFilePath()).readAsStringSync(); } - SharedPreferencesWindows _getPreferences() { + SharedPreferencesWindows getPreferences() { final SharedPreferencesWindows prefs = SharedPreferencesWindows(); prefs.fs = fileSystem; prefs.pathProvider = pathProvider; @@ -48,8 +48,8 @@ void main() { }); test('getAll', () async { - await _writeTestFile('{"key1": "one", "key2": 2}'); - final SharedPreferencesWindows prefs = _getPreferences(); + await writeTestFile('{"key1": "one", "key2": 2}'); + final SharedPreferencesWindows prefs = getPreferences(); final Map values = await prefs.getAll(); expect(values, hasLength(2)); @@ -58,30 +58,30 @@ void main() { }); test('remove', () async { - await _writeTestFile('{"key1":"one","key2":2}'); - final SharedPreferencesWindows prefs = _getPreferences(); + await writeTestFile('{"key1":"one","key2":2}'); + final SharedPreferencesWindows prefs = getPreferences(); await prefs.remove('key2'); - expect(await _readTestFile(), '{"key1":"one"}'); + expect(await readTestFile(), '{"key1":"one"}'); }); test('setValue', () async { - await _writeTestFile('{}'); - final SharedPreferencesWindows prefs = _getPreferences(); + await writeTestFile('{}'); + final SharedPreferencesWindows prefs = getPreferences(); await prefs.setValue('', 'key1', 'one'); await prefs.setValue('', 'key2', 2); - expect(await _readTestFile(), '{"key1":"one","key2":2}'); + expect(await readTestFile(), '{"key1":"one","key2":2}'); }); test('clear', () async { - await _writeTestFile('{"key1":"one","key2":2}'); - final SharedPreferencesWindows prefs = _getPreferences(); + await writeTestFile('{"key1":"one","key2":2}'); + final SharedPreferencesWindows prefs = getPreferences(); await prefs.clear(); - expect(await _readTestFile(), '{}'); + expect(await readTestFile(), '{}'); }); } diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 1634dcdab0f9..18a0289eb43f 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,50 @@ +## 6.1.6 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 6.1.5 + +* Migrates `README.md` examples to the [`code-excerpt` system](https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#readme-code). + +## 6.1.4 + +* Adopts new platform interface method for launching URLs. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/105648). + +## 6.1.3 + +* Updates README section about query permissions to better reflect changes to + `canLaunchUrl` recommendations. + +## 6.1.2 + +* Minor fixes for new analysis options. + +## 6.1.1 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 6.1.0 + +* Introduces new `launchUrl` and `canLaunchUrl` APIs; `launch` and `canLaunch` + are now deprecated. These new APIs: + * replace the `String` URL argument with a `Uri`, to prevent common issues + with providing invalid URL strings. + * replace `forceSafariVC` and `forceWebView` with `LaunchMode`, which makes + the API platform-neutral, and standardizes the default behavior between + Android and iOS. + * move web view configuration options into a new `WebViewConfiguration` + object. The default behavior for JavaScript and DOM storage is now enabled + rather than disabled. +* Also deprecates `closeWebView` in favor of `closeInAppWebView` to clarify + that it is specific to the in-app web view launch option. +* Adds OS version support information to README. +* Reorganizes and clarifies README. + ## 6.0.20 * Fixes a typo in `default_package` registration for Windows, macOS, and Linux. diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index a4ffb6241f6c..e9e4dae476cc 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -1,9 +1,14 @@ + + # url_launcher [![pub package](https://img.shields.io/pub/v/url_launcher.svg)](https://pub.dev/packages/url_launcher) -A Flutter plugin for launching a URL. Supports -iOS, Android, web, Windows, macOS, and Linux. +A Flutter plugin for launching a URL. + +| | Android | iOS | Linux | macOS | Web | Windows | +|-------------|---------|------|-------|--------|-----|-------------| +| **Support** | SDK 16+ | 9.0+ | Any | 10.11+ | Any | Windows 10+ | ## Usage @@ -11,18 +16,19 @@ To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml fil ### Example + ``` dart import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; -const String _url = 'https://flutter.dev'; +final Uri _url = Uri.parse('https://flutter.dev'); void main() => runApp( const MaterialApp( home: Material( child: Center( - child: RaisedButton( - onPressed: _launchURL, + child: ElevatedButton( + onPressed: _launchUrl, child: Text('Show Flutter homepage'), ), ), @@ -30,8 +36,10 @@ void main() => runApp( ), ); -void _launchURL() async { - if (!await launch(_url)) throw 'Could not launch $_url'; +Future _launchUrl() async { + if (!await launchUrl(_url)) { + throw 'Could not launch $_url'; + } } ``` @@ -40,14 +48,15 @@ See the example app for more complex examples. ## Configuration ### iOS -Add any URL schemes passed to `canLaunch` as `LSApplicationQueriesSchemes` entries in your Info.plist file. +Add any URL schemes passed to `canLaunchUrl` as `LSApplicationQueriesSchemes` +entries in your Info.plist file, otherwise it will return false. Example: -``` +```xml LSApplicationQueriesSchemes - https - http + sms + tel ``` @@ -55,136 +64,156 @@ See [`-[UIApplication canOpenURL:]`](https://developer.apple.com/documentation/u ### Android -Starting from API 30 Android requires package visibility configuration in your -`AndroidManifest.xml` otherwise `canLaunch` will return `false`. A `` +Add any URL schemes passed to `canLaunchUrl` as `` entries in your +`AndroidManifest.xml`, otherwise it will return false in most cases starting +on Android 11 (API 30) or higher. A `` element must be added to your manifest as a child of the root element. -The snippet below shows an example for an application that uses `https`, `tel`, -and `mailto` URLs with `url_launcher`. See -[the Android documentation](https://developer.android.com/training/package-visibility/use-cases) -for examples of other queries. +Example: + ``` xml + - + - + - + - + - - - - - - - - - - ``` +See +[the Android documentation](https://developer.android.com/training/package-visibility/use-cases) +for examples of other queries. + ## Supported URL schemes -The [`launch`](https://pub.dev/documentation/url_launcher/latest/url_launcher/launch.html) method -takes a string argument containing a URL. This URL -can be formatted using a number of different URL schemes. The supported -URL schemes depend on the underlying platform and installed apps. +The provided URL is passed directly to the host platform for handling. The +supported URL schemes therefore depend on the platform and installed apps. Commonly used schemes include: | Scheme | Example | Action | |:---|:---|:---| -| `https:` | `https://flutter.dev` | Open URL in the default browser | -| `mailto:?subject=&body=` | `mailto:smith@example.org?subject=News&body=New%20plugin` | Create email to in the default email app | -| `tel:` | `tel:+1-555-010-999` | Make a phone call to using the default phone app | -| `sms:` | `sms:5550101234` | Send an SMS message to using the default messaging app | +| `https:` | `https://flutter.dev` | Open `` in the default browser | +| `mailto:?subject=&body=` | `mailto:smith@example.org?subject=News&body=New%20plugin` | Create email to `` in the default email app | +| `tel:` | `tel:+1-555-010-999` | Make a phone call to `` using the default phone app | +| `sms:` | `sms:5550101234` | Send an SMS message to `` using the default messaging app | | `file:` | `file:/home` | Open file or folder using default app association, supported on desktop platforms | More details can be found here for [iOS](https://developer.apple.com/library/content/featuredarticles/iPhoneURLScheme_Reference/Introduction/Introduction.html) and [Android](https://developer.android.com/guide/components/intents-common.html) -**Note**: URL schemes are only supported if there are apps installed on the device that can +URL schemes are only supported if there are apps installed on the device that can support them. For example, iOS simulators don't have a default email or phone apps installed, so can't open `tel:` or `mailto:` links. +### Checking supported schemes + +If you need to know at runtime whether a scheme is guaranteed to work before +using it (for instance, to adjust your UI based on what is available), you can +check with [`canLaunchUrl`](https://pub.dev/documentation/url_launcher/latest/url_launcher/canLaunchUrl.html). + +However, `canLaunchUrl` can return false even if `launchUrl` would work in +some circumstances (in web applications, on mobile without the necessary +configuration as described above, etc.), so in cases where you can provide +fallback behavior it is better to use `launchUrl` directly and handle failure. +For example, a UI button that would have sent feedback email using a `mailto` URL +might instead open a web-based feedback form using an `https` URL on failure, +rather than disabling the button if `canLaunchUrl` returns false for `mailto`. + ### Encoding URLs URLs must be properly encoded, especially when including spaces or other special -characters. This can be done using the +characters. In general this is handled automatically by the [`Uri` class](https://api.dart.dev/dart-core/Uri-class.html). -For example: + +**However**, for any scheme other than `http` or `https`, you should use the +`query` parameter and the `encodeQueryParameters` function shown below rather +than `Uri`'s `queryParameters` constructor argument for any query parameters, +due to [a bug](https://github.com/dart-lang/sdk/issues/43838) in the way `Uri` +encodes query parameters. Using `queryParameters` will result in spaces being +converted to `+` in many cases. + + ```dart String? encodeQueryParameters(Map params) { return params.entries - .map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') + .map((MapEntry e) => + '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') .join('&'); } +// ··· + final Uri emailLaunchUri = Uri( + scheme: 'mailto', + path: 'smith@example.com', + query: encodeQueryParameters({ + 'subject': 'Example Subject & Symbols are allowed!', + }), + ); + + launchUrl(emailLaunchUri); +``` -final Uri emailLaunchUri = Uri( - scheme: 'mailto', - path: 'smith@example.com', - query: encodeQueryParameters({ - 'subject': 'Example Subject & Symbols are allowed!' - }), -); +Encoding for `sms` is slightly different: -launch(emailLaunchUri.toString()); + +```dart +final Uri smsLaunchUri = Uri( + scheme: 'sms', + path: '0118 999 881 999 119 7253', + queryParameters: { + 'body': Uri.encodeComponent('Example Subject & Symbols are allowed!'), + }, +); ``` -**Warning**: For any scheme other than `http` or `https`, you should use the -`query` parameter and the `encodeQueryParameters` function shown above rather -than `Uri`'s `queryParameters` constructor argument, due to -[a bug](https://github.com/dart-lang/sdk/issues/43838) in the way `Uri` -encodes query parameters. Using `queryParameters` will result in spaces being -converted to `+` in many cases. +### URLs not handled by `Uri` -### Handling missing URL receivers +In rare cases, you may need to launch a URL that the host system considers +valid, but cannot be expressed by `Uri`. For those cases, alternate APIs using +strings are available by importing `url_launcher_string.dart`. -A particular mobile device may not be able to receive all supported URL schemes. -For example, a tablet may not have a cellular radio and thus no support for -launching a URL using the `sms` scheme, or a device may not have an email app -and thus no support for launching a URL using the `mailto` scheme. +Using these APIs in any other cases is **strongly discouraged**, as providing +invalid URL strings was a very common source of errors with this plugin's +original APIs. -We recommend checking which URL schemes are supported using the -[`canLaunch`](https://pub.dev/documentation/url_launcher/latest/url_launcher/canLaunch.html) -in most cases. If the `canLaunch` method returns false, as a -best practice we suggest adjusting the application UI so that the unsupported -URL is never triggered; for example, if the `mailto` scheme is not supported, a -UI button that would have sent feedback email could be changed to instead open -a web-based feedback form using an `https` URL. +### File scheme handling -## Browser vs In-app Handling -By default, Android opens up a browser when handling URLs. You can pass -`forceWebView: true` parameter to tell the plugin to open a WebView instead. -If you do this for a URL of a page containing JavaScript, make sure to pass in -`enableJavaScript: true`, or else the launch method will not work properly. On -iOS, the default behavior is to open all web URLs within the app. Everything -else is redirected to the app handler. +`file:` scheme can be used on desktop platforms: Windows, macOS, and Linux. -## File scheme handling -`file:` scheme can be used on desktop platforms: `macOS`, `Linux` and `Windows`. - -We recommend checking first whether the directory or file exists before calling `launch`. +We recommend checking first whether the directory or file exists before calling `launchUrl`. Example: + + ```dart -var filePath = '/path/to/file'; +final String filePath = testFile.absolute.path; final Uri uri = Uri.file(filePath); -if (await File(uri.toFilePath()).exists()) { - if (!await launch(uri.toString())) { - throw 'Could not launch $uri'; - } +if (!File(uri.toFilePath()).existsSync()) { + throw '$uri does not exist!'; +} +if (!await launchUrl(uri)) { + throw 'Could not launch $uri'; } ``` -### macOS file access configuration +#### macOS file access configuration -If you need to access files outside of your application's sandbox, you will need to have the necessary +If you need to access files outside of your application's sandbox, you will need to have the necessary [entitlements](https://docs.flutter.dev/desktop#entitlements-and-the-app-sandbox). + +## Browser vs in-app Handling + +On some platforms, web URLs can be launched either in an in-app web view, or +in the default browser. The default behavior depends on the platform (see +[`launchUrl`](https://pub.dev/documentation/url_launcher/latest/url_launcher/launchUrl.html) +for details), but a specific mode can be used on supported platforms by +passing a `LaunchMode`. diff --git a/packages/url_launcher/url_launcher/example/README.md b/packages/url_launcher/url_launcher/example/README.md index c200da8974d1..35b4bdb7031e 100644 --- a/packages/url_launcher/url_launcher/example/README.md +++ b/packages/url_launcher/url_launcher/example/README.md @@ -1,8 +1,3 @@ # url_launcher_example Demonstrates how to use the url_launcher plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/url_launcher/url_launcher/example/android/app/build.gradle b/packages/url_launcher/url_launcher/example/android/app/build.gradle index ca41cb14b6f4..8c7e84563ee6 100644 --- a/packages/url_launcher/url_launcher/example/android/app/build.gradle +++ b/packages/url_launcher/url_launcher/example/android/app/build.gradle @@ -54,7 +54,7 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } diff --git a/packages/url_launcher/url_launcher/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/url_launcher/url_launcher/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/url_launcher/url_launcher/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/url_launcher/url_launcher/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml b/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml index fa149f94adf0..fe01f2fba9a8 100644 --- a/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml +++ b/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml @@ -7,21 +7,29 @@ --> + + - + + - + + + - - + + + + runApp( + const MaterialApp( + home: Material( + child: Center( + child: ElevatedButton( + onPressed: _launchUrl, + child: Text('Show Flutter homepage'), + ), + ), + ), + ), + ); + +Future _launchUrl() async { + if (!await launchUrl(_url)) { + throw '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 new file mode 100644 index 000000000000..24c724466a77 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/lib/encoding.dart @@ -0,0 +1,70 @@ +// 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. + +// Run this example with: flutter run -t lib/encoding.dart -d emulator + +// This file is used to extract code samples for the README.md file. +// Run update-excerpts if you modify this file. + +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +/// Encode [params] so it produces a correct query string. +/// Workaround for: https://github.com/dart-lang/sdk/issues/43838 +// #docregion encode-query-parameters +String? encodeQueryParameters(Map params) { + return params.entries + .map((MapEntry e) => + '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') + .join('&'); +} +// #enddocregion encode-query-parameters + +void main() => runApp( + MaterialApp( + home: Material( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: const [ + ElevatedButton( + onPressed: _composeMail, + child: Text('Compose an email'), + ), + ElevatedButton( + onPressed: _composeSms, + child: Text('Compose a SMS'), + ), + ], + ), + ), + ), + ); + +void _composeMail() { +// #docregion encode-query-parameters + final Uri emailLaunchUri = Uri( + scheme: 'mailto', + path: 'smith@example.com', + query: encodeQueryParameters({ + 'subject': 'Example Subject & Symbols are allowed!', + }), + ); + + launchUrl(emailLaunchUri); +// #enddocregion encode-query-parameters +} + +void _composeSms() { +// #docregion sms + final Uri smsLaunchUri = Uri( + scheme: 'sms', + path: '0118 999 881 999 119 7253', + queryParameters: { + 'body': Uri.encodeComponent('Example Subject & Symbols are allowed!'), + }, + ); +// #enddocregion sms + + launchUrl(smsLaunchUri); +} diff --git a/packages/url_launcher/url_launcher/example/lib/files.dart b/packages/url_launcher/url_launcher/example/lib/files.dart new file mode 100644 index 000000000000..d48440670406 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/lib/files.dart @@ -0,0 +1,47 @@ +// 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. + +// Run this example with: flutter run -t lib/files.dart -d linux + +// This file is used to extract code samples for the README.md file. +// Run update-excerpts if you modify this file. +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as p; +import 'package:url_launcher/url_launcher.dart'; + +void main() => runApp( + const MaterialApp( + home: Material( + child: Center( + child: ElevatedButton( + onPressed: _openFile, + child: Text('Open File'), + ), + ), + ), + ), + ); + +Future _openFile() async { + // Prepare a file within tmp + final String tempFilePath = p.joinAll([ + ...p.split(Directory.systemTemp.path), + 'flutter_url_launcher_example.txt' + ]); + final File testFile = File(tempFilePath); + await testFile.writeAsString('Hello, world!'); +// #docregion file + final String filePath = testFile.absolute.path; + final Uri uri = Uri.file(filePath); + + if (!File(uri.toFilePath()).existsSync()) { + throw '$uri does not exist!'; + } + if (!await launchUrl(uri)) { + throw '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 a5e38ceecc84..a538940f1a68 100644 --- a/packages/url_launcher/url_launcher/example/lib/main.dart +++ b/packages/url_launcher/url_launcher/example/lib/main.dart @@ -11,10 +11,12 @@ import 'package:url_launcher/link.dart'; import 'package:url_launcher/url_launcher.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -32,7 +34,7 @@ class MyHomePage extends StatefulWidget { final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { @@ -44,67 +46,62 @@ class _MyHomePageState extends State { void initState() { super.initState(); // Check for phone call support. - canLaunch('tel:123').then((bool result) { + canLaunchUrl(Uri(scheme: 'tel', path: '123')).then((bool result) { setState(() { _hasCallSupport = result; }); }); } - Future _launchInBrowser(String url) async { - if (!await launch( + Future _launchInBrowser(Uri url) async { + if (!await launchUrl( url, - forceSafariVC: false, - forceWebView: false, - headers: {'my_header_key': 'my_header_value'}, + mode: LaunchMode.externalApplication, )) { throw 'Could not launch $url'; } } - Future _launchInWebViewOrVC(String url) async { - if (!await launch( + Future _launchInWebViewOrVC(Uri url) async { + if (!await launchUrl( url, - forceSafariVC: true, - forceWebView: true, - headers: {'my_header_key': 'my_header_value'}, + mode: LaunchMode.inAppWebView, + webViewConfiguration: const WebViewConfiguration( + headers: {'my_header_key': 'my_header_value'}), )) { throw 'Could not launch $url'; } } - Future _launchInWebViewWithJavaScript(String url) async { - if (!await launch( + Future _launchInWebViewWithoutJavaScript(Uri url) async { + if (!await launchUrl( url, - forceSafariVC: true, - forceWebView: true, - enableJavaScript: true, + mode: LaunchMode.inAppWebView, + webViewConfiguration: const WebViewConfiguration(enableJavaScript: false), )) { throw 'Could not launch $url'; } } - Future _launchInWebViewWithDomStorage(String url) async { - if (!await launch( + Future _launchInWebViewWithoutDomStorage(Uri url) async { + if (!await launchUrl( url, - forceSafariVC: true, - forceWebView: true, - enableDomStorage: true, + mode: LaunchMode.inAppWebView, + webViewConfiguration: const WebViewConfiguration(enableDomStorage: false), )) { throw 'Could not launch $url'; } } - Future _launchUniversalLinkIos(String url) async { - final bool nativeAppLaunchSucceeded = await launch( + Future _launchUniversalLinkIos(Uri url) async { + final bool nativeAppLaunchSucceeded = await launchUrl( url, - forceSafariVC: false, - universalLinksOnly: true, + mode: LaunchMode.externalNonBrowserApplication, ); if (!nativeAppLaunchSucceeded) { - await launch( + await launchUrl( url, - forceSafariVC: true, + mode: LaunchMode.inAppWebView, ); } } @@ -118,22 +115,19 @@ class _MyHomePageState extends State { } Future _makePhoneCall(String phoneNumber) async { - // Use `Uri` to ensure that `phoneNumber` is properly URL-encoded. - // Just using 'tel:$phoneNumber' would create invalid URLs in some cases, - // such as spaces in the input, which would cause `launch` to fail on some - // platforms. final Uri launchUri = Uri( scheme: 'tel', path: phoneNumber, ); - await launch(launchUri.toString()); + await launchUrl(launchUri); } @override Widget build(BuildContext context) { // onPressed calls using this URL are not gated on a 'canLaunch' check // because the assumption is that every device can launch a web URL. - const String toLaunch = 'https://www.cylog.org/headers/'; + final Uri toLaunch = + Uri(scheme: 'https', host: 'www.cylog.org', path: 'headers/'); return Scaffold( appBar: AppBar( title: Text(widget.title), @@ -160,9 +154,9 @@ class _MyHomePageState extends State { ? const Text('Make phone call') : const Text('Calling not supported'), ), - const Padding( - padding: EdgeInsets.all(16.0), - child: Text(toLaunch), + Padding( + padding: const EdgeInsets.all(16.0), + child: Text(toLaunch.toString()), ), ElevatedButton( onPressed: () => setState(() { @@ -179,15 +173,15 @@ class _MyHomePageState extends State { ), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInWebViewWithJavaScript(toLaunch); + _launched = _launchInWebViewWithoutJavaScript(toLaunch); }), - child: const Text('Launch in app(JavaScript ON)'), + child: const Text('Launch in app (JavaScript OFF)'), ), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInWebViewWithDomStorage(toLaunch); + _launched = _launchInWebViewWithoutDomStorage(toLaunch); }), - child: const Text('Launch in app(DOM storage ON)'), + child: const Text('Launch in app (DOM storage OFF)'), ), const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( @@ -203,7 +197,7 @@ class _MyHomePageState extends State { _launched = _launchInWebViewOrVC(toLaunch); Timer(const Duration(seconds: 5), () { print('Closing WebView after 5 seconds...'); - closeWebView(); + closeInAppWebView(); }); }), child: const Text('Launch in app + close after 5 seconds'), diff --git a/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugins.cmake b/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugins.cmake index 1fc8ed344297..f16b4c34213a 100644 --- a/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugins.cmake +++ b/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST url_launcher_linux ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/url_launcher/url_launcher/example/pubspec.yaml b/packages/url_launcher/url_launcher/example/pubspec.yaml index 3b2bba9833a3..573dc0d9ed01 100644 --- a/packages/url_launcher/url_launcher/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher/example/pubspec.yaml @@ -4,11 +4,12 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" dependencies: flutter: sdk: flutter + path: ^1.8.0 url_launcher: # When depending on this package from a real application you should use: # url_launcher: ^x.y.z @@ -18,8 +19,11 @@ dependencies: path: ../ dev_dependencies: + build_runner: ^2.1.10 flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter mockito: ^5.0.0 diff --git a/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugins.cmake b/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugins.cmake index 411af46dd721..88b22e5c775e 100644 --- a/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugins.cmake +++ b/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST url_launcher_windows ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/url_launcher/url_launcher/lib/src/legacy_api.dart b/packages/url_launcher/url_launcher/lib/src/legacy_api.dart new file mode 100644 index 000000000000..f6faf3fa3d0e --- /dev/null +++ b/packages/url_launcher/url_launcher/lib/src/legacy_api.dart @@ -0,0 +1,154 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +/// Parses the specified URL string and delegates handling of it to the +/// underlying platform. +/// +/// The returned future completes with a [PlatformException] on invalid URLs and +/// schemes which cannot be handled, that is when [canLaunch] would complete +/// with false. +/// +/// By default when [forceSafariVC] is unset, the launcher +/// opens web URLs in the Safari View Controller, anything else is opened +/// using the default handler on the platform. If set to true, it opens the +/// URL in the Safari View Controller. If false, the URL is opened in the +/// default browser of the phone. Note that to work with universal links on iOS, +/// this must be set to false to let the platform's system handle the URL. +/// Set this to false if you want to use the cookies/context of the main browser +/// of the app (such as SSO flows). This setting will nullify [universalLinksOnly] +/// and will always launch a web content in the built-in Safari View Controller regardless +/// if the url is a universal link or not. +/// +/// [universalLinksOnly] is only used in iOS with iOS version >= 10.0. This setting is only validated +/// when [forceSafariVC] is set to false. The default value of this setting is false. +/// By default (when unset), the launcher will either launch the url in a browser (when the +/// url is not a universal link), or launch the respective native app content (when +/// the url is a universal link). When set to true, the launcher will only launch +/// the content if the url is a universal link and the respective app for the universal +/// link is installed on the user's device; otherwise throw a [PlatformException]. +/// +/// [forceWebView] is an Android only setting. If null or false, the URL is +/// always launched with the default browser on device. If set to true, the URL +/// is launched in a WebView. Unlike iOS, browser context is shared across +/// WebViews. +/// [enableJavaScript] is an Android only setting. If true, WebView enable +/// javascript. +/// [enableDomStorage] is an Android only setting. If true, WebView enable +/// DOM storage. +/// [headers] is an Android only setting that adds headers to the WebView. +/// When not using a WebView, the header information is passed to the browser, +/// some Android browsers do not support the [Browser.EXTRA_HEADERS](https://developer.android.com/reference/android/provider/Browser#EXTRA_HEADERS) +/// intent extra and the header information will be lost. +/// [webOnlyWindowName] is an Web only setting . _blank opens the new url in new tab , +/// _self opens the new url in current tab. +/// Default behaviour is to open the url in new tab. +/// +/// Note that if any of the above are set to true but the URL is not a web URL, +/// this will throw a [PlatformException]. +/// +/// [statusBarBrightness] Sets the status bar brightness of the application +/// after opening a link on iOS. Does nothing if no value is passed. This does +/// not handle resetting the previous status bar style. +/// +/// Returns true if launch url is successful; false is only returned when [universalLinksOnly] +/// is set to true and the universal link failed to launch. +@Deprecated('Use launchUrl instead') +Future launch( + String urlString, { + bool? forceSafariVC, + bool forceWebView = false, + bool enableJavaScript = false, + bool enableDomStorage = false, + bool universalLinksOnly = false, + Map headers = const {}, + Brightness? statusBarBrightness, + String? webOnlyWindowName, +}) async { + final Uri? url = Uri.tryParse(urlString.trimLeft()); + final bool isWebURL = + url != null && (url.scheme == 'http' || url.scheme == 'https'); + + if ((forceSafariVC ?? false || forceWebView) && !isWebURL) { + throw PlatformException( + code: 'NOT_A_WEB_SCHEME', + message: 'To use webview or safariVC, you need to pass ' + 'in a web URL. This $urlString is not a web URL.'); + } + + /// [true] so that ui is automatically computed if [statusBarBrightness] is set. + bool previousAutomaticSystemUiAdjustment = true; + if (statusBarBrightness != null && + defaultTargetPlatform == TargetPlatform.iOS && + _ambiguate(WidgetsBinding.instance) != null) { + previousAutomaticSystemUiAdjustment = _ambiguate(WidgetsBinding.instance)! + .renderView + .automaticSystemUiAdjustment; + _ambiguate(WidgetsBinding.instance)! + .renderView + .automaticSystemUiAdjustment = false; + SystemChrome.setSystemUIOverlayStyle(statusBarBrightness == Brightness.light + ? SystemUiOverlayStyle.dark + : SystemUiOverlayStyle.light); + } + + final bool result = await UrlLauncherPlatform.instance.launch( + urlString, + useSafariVC: forceSafariVC ?? isWebURL, + useWebView: forceWebView, + enableJavaScript: enableJavaScript, + enableDomStorage: enableDomStorage, + universalLinksOnly: universalLinksOnly, + headers: headers, + webOnlyWindowName: webOnlyWindowName, + ); + + if (statusBarBrightness != null && + _ambiguate(WidgetsBinding.instance) != null) { + _ambiguate(WidgetsBinding.instance)! + .renderView + .automaticSystemUiAdjustment = previousAutomaticSystemUiAdjustment; + } + + return result; +} + +/// Checks whether the specified URL can be handled by some app installed on the +/// device. +/// +/// On some systems, such as recent versions of Android and iOS, this will +/// always return false unless the application has been configuration to allow +/// querying the system for launch support. See +/// [the README](https://pub.dev/packages/url_launcher#configuration) for +/// details. +@Deprecated('Use canLaunchUrl instead') +Future canLaunch(String urlString) async { + return await UrlLauncherPlatform.instance.canLaunch(urlString); +} + +/// Closes the current WebView, if one was previously opened via a call to [launch]. +/// +/// If [launch] was never called, then this call will not have any effect. +/// +/// On Android systems, if [launch] was called without `forceWebView` being set to `true` +/// Or on IOS systems, if [launch] was called without `forceSafariVC` being set to `true`, +/// this call will not do anything either, simply because there is no +/// WebView/SafariViewController available to be closed. +@Deprecated('Use closeInAppWebView instead') +Future closeWebView() async { + return await UrlLauncherPlatform.instance.closeWebView(); +} + +/// 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. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher/lib/src/link.dart b/packages/url_launcher/url_launcher/lib/src/link.dart index 016f97daacbf..91f7389ff251 100644 --- a/packages/url_launcher/url_launcher/lib/src/link.dart +++ b/packages/url_launcher/url_launcher/lib/src/link.dart @@ -6,10 +6,12 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher_platform_interface/link.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +import 'types.dart'; +import 'url_launcher_uri.dart'; + /// The function used to push routes to the Flutter framework. @visibleForTesting Future Function(Object?, String) pushRouteToFrameworkFunction = @@ -84,7 +86,7 @@ class Link extends StatelessWidget implements LinkInfo { /// event channel messages to instruct the framework to push the route name. class DefaultLinkDelegate extends StatelessWidget { /// Creates a delegate for the given [link]. - const DefaultLinkDelegate(this.link); + const DefaultLinkDelegate(this.link, {Key? key}) : super(key: key); /// Given a [link], creates an instance of [DefaultLinkDelegate]. /// @@ -107,7 +109,8 @@ class DefaultLinkDelegate extends StatelessWidget { } Future _followLink(BuildContext context) async { - if (!link.uri!.hasScheme) { + final Uri url = link.uri!; + if (!url.hasScheme) { // A uri that doesn't have a scheme is an internal route name. In this // case, we push it via Flutter's navigation system instead of letting the // browser handle it. @@ -116,18 +119,18 @@ class DefaultLinkDelegate extends StatelessWidget { return; } - // At this point, we know that the link is external. So we use the `launch` - // API to open the link. - final String urlString = link.uri.toString(); - if (await canLaunch(urlString)) { - await launch( - urlString, - forceSafariVC: _useWebView, - forceWebView: _useWebView, + // At this point, we know that the link is external. So we use the + // `launchUrl` API to open the link. + if (await canLaunchUrl(url)) { + await launchUrl( + url, + mode: _useWebView + ? LaunchMode.inAppWebView + : LaunchMode.externalApplication, ); } else { FlutterError.reportError(FlutterErrorDetails( - exception: 'Could not launch link $urlString', + exception: 'Could not launch link $url', stack: StackTrace.current, library: 'url_launcher', context: ErrorDescription('during launching a link'), diff --git a/packages/url_launcher/url_launcher/lib/src/type_conversion.dart b/packages/url_launcher/url_launcher/lib/src/type_conversion.dart new file mode 100644 index 000000000000..970f04dced57 --- /dev/null +++ b/packages/url_launcher/url_launcher/lib/src/type_conversion.dart @@ -0,0 +1,32 @@ +// 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:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +import 'types.dart'; + +/// Converts an (app-facing) [WebViewConfiguration] to a (platform interface) +/// [InAppWebViewConfiguration]. +InAppWebViewConfiguration convertConfiguration(WebViewConfiguration config) { + return InAppWebViewConfiguration( + enableJavaScript: config.enableJavaScript, + enableDomStorage: config.enableDomStorage, + headers: config.headers, + ); +} + +/// Converts an (app-facing) [LaunchMode] to a (platform interface) +/// [PreferredLaunchMode]. +PreferredLaunchMode convertLaunchMode(LaunchMode mode) { + switch (mode) { + case LaunchMode.platformDefault: + return PreferredLaunchMode.platformDefault; + case LaunchMode.inAppWebView: + return PreferredLaunchMode.inAppWebView; + case LaunchMode.externalApplication: + return PreferredLaunchMode.externalApplication; + case LaunchMode.externalNonBrowserApplication: + return PreferredLaunchMode.externalNonBrowserApplication; + } +} diff --git a/packages/url_launcher/url_launcher/lib/src/types.dart b/packages/url_launcher/url_launcher/lib/src/types.dart new file mode 100644 index 000000000000..bcfcb7887b17 --- /dev/null +++ b/packages/url_launcher/url_launcher/lib/src/types.dart @@ -0,0 +1,54 @@ +// 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'; + +/// The desired mode to launch a URL. +/// +/// Support for these modes varies by platform. Platforms that do not support +/// the requested mode may substitute another mode. See [launchUrl] for more +/// details. +enum LaunchMode { + /// Leaves the decision of how to launch the URL to the platform + /// implementation. + platformDefault, + + /// Loads the URL in an in-app web view (e.g., Safari View Controller). + inAppWebView, + + /// Passes the URL to the OS to be handled by another application. + externalApplication, + + /// Passes the URL to the OS to be handled by another non-browser application. + externalNonBrowserApplication, +} + +/// Additional configuration options for [LaunchMode.inAppWebView]. +@immutable +class WebViewConfiguration { + /// Creates a new WebViewConfiguration with the given settings. + const WebViewConfiguration({ + this.enableJavaScript = true, + this.enableDomStorage = true, + this.headers = const {}, + }); + + /// Whether or not JavaScript is enabled for the web content. + /// + /// Disabling this may not be supported on all platforms. + final bool enableJavaScript; + + /// Whether or not DOM storage is enabled for the web content. + /// + /// Disabling this may not be supported on all platforms. + final bool enableDomStorage; + + /// Additional headers to pass in the load request. + /// + /// On Android, this may work even when not loading in an in-app web view. + /// When loading in an external browsers, this sets + /// [Browser.EXTRA_HEADERS](https://developer.android.com/reference/android/provider/Browser#EXTRA_HEADERS) + /// Not all browsers support this, so it is not guaranteed to be honored. + final Map headers; +} diff --git a/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart b/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart new file mode 100644 index 000000000000..cf96ebc095da --- /dev/null +++ b/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart @@ -0,0 +1,57 @@ +// 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:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +import 'type_conversion.dart'; +import 'types.dart'; + +/// String version of [launchUrl]. +/// +/// This should be used only in the very rare case of needing to launch a URL +/// that is considered valid by the host platform, but not by Dart's [Uri] +/// class. In all other cases, use [launchUrl] instead, as that will ensure +/// that you are providing a valid URL. +/// +/// The behavior of this method when passing an invalid URL is entirely +/// platform-specific; no effort is made by the plugin to make the URL valid. +/// Some platforms may provide best-effort interpretation of an invalid URL, +/// others will immediately fail if the URL can't be parsed according to the +/// official standards that define URL formats. +Future launchUrlString( + String urlString, { + LaunchMode mode = LaunchMode.platformDefault, + WebViewConfiguration webViewConfiguration = const WebViewConfiguration(), + String? webOnlyWindowName, +}) async { + if (mode == LaunchMode.inAppWebView && + !(urlString.startsWith('https:') || urlString.startsWith('http:'))) { + throw ArgumentError.value(urlString, 'urlString', + 'To use an in-app web view, you must provide an http(s) URL.'); + } + return await UrlLauncherPlatform.instance.launchUrl( + urlString, + LaunchOptions( + mode: convertLaunchMode(mode), + webViewConfiguration: convertConfiguration(webViewConfiguration), + webOnlyWindowName: webOnlyWindowName, + ), + ); +} + +/// String version of [canLaunchUrl]. +/// +/// This should be used only in the very rare case of needing to check a URL +/// that is considered valid by the host platform, but not by Dart's [Uri] +/// class. In all other cases, use [canLaunchUrl] instead, as that will ensure +/// that you are providing a valid URL. +/// +/// The behavior of this method when passing an invalid URL is entirely +/// platform-specific; no effort is made by the plugin to make the URL valid. +/// Some platforms may provide best-effort interpretation of an invalid URL, +/// others will immediately fail if the URL can't be parsed according to the +/// official standards that define URL formats. +Future canLaunchUrlString(String urlString) async { + return await UrlLauncherPlatform.instance.canLaunch(urlString); +} diff --git a/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart b/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart new file mode 100644 index 000000000000..30321026ceb9 --- /dev/null +++ b/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart @@ -0,0 +1,88 @@ +// 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:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +import '../url_launcher_string.dart'; +import 'type_conversion.dart'; + +/// Passes [url] to the underlying platform for handling. +/// +/// [mode] support varies significantly by platform: +/// - [LaunchMode.platformDefault] is supported on all platforms: +/// - On iOS and Android, this treats web URLs as +/// [LaunchMode.inAppWebView], and all other URLs as +/// [LaunchMode.externalApplication]. +/// - On Windows, macOS, and Linux this behaves like +/// [LaunchMode.externalApplication]. +/// - On web, this uses `webOnlyWindowName` for web URLs, and behaves like +/// [LaunchMode.externalApplication] for any other content. +/// - [LaunchMode.inAppWebView] is currently only supported on iOS and +/// Android. If a non-web URL is passed with this mode, an [ArgumentError] +/// will be thrown. +/// - [LaunchMode.externalApplication] is supported on all platforms. +/// On iOS, this should be used in cases where sharing the cookies of the +/// user's browser is important, such as SSO flows, since Safari View +/// Controller does not share the browser's context. +/// - [LaunchMode.externalNonBrowserApplication] is supported on iOS 10+. +/// This setting is used to require universal links to open in a non-browser +/// application. +/// +/// For web, [webOnlyWindowName] specifies a target for the launch. This +/// supports the standard special link target names. For example: +/// - "_blank" opens the new URL in a new tab. +/// - "_self" opens the new URL in the current tab. +/// Default behaviour when unset is to open the url in a new tab. +/// +/// Returns true if the URL was launched successful, otherwise either returns +/// false or throws a [PlatformException] depending on the failure. +Future launchUrl( + Uri url, { + LaunchMode mode = LaunchMode.platformDefault, + WebViewConfiguration webViewConfiguration = const WebViewConfiguration(), + String? webOnlyWindowName, +}) async { + if (mode == LaunchMode.inAppWebView && + !(url.scheme == 'https' || url.scheme == 'http')) { + throw ArgumentError.value(url, 'url', + 'To use an in-app web view, you must provide an http(s) URL.'); + } + return await UrlLauncherPlatform.instance.launchUrl( + url.toString(), + LaunchOptions( + mode: convertLaunchMode(mode), + webViewConfiguration: convertConfiguration(webViewConfiguration), + webOnlyWindowName: webOnlyWindowName, + ), + ); +} + +/// Checks whether the specified URL can be handled by some app installed on the +/// device. +/// +/// Returns true if it is possible to verify that there is a handler available. +/// A false return value can indicate either that there is no handler available, +/// or that the application does not have permission to check. For example: +/// - On recent versions of Android and iOS, this will always return false +/// unless the application has been configuration to allow +/// querying the system for launch support. See +/// [the README](https://pub.dev/packages/url_launcher#configuration) for +/// details. +/// - On web, this will always return false except for a few specific schemes +/// that are always assumed to be supported (such as http(s)), as web pages +/// are never allowed to query installed applications. +Future canLaunchUrl(Uri url) async { + return await UrlLauncherPlatform.instance.canLaunch(url.toString()); +} + +/// Closes the current in-app web view, if one was previously opened by +/// [launchUrl]. +/// +/// If [launchUrl] was never called with [LaunchMode.inAppWebView], then this +/// call will have no effect. +Future closeInAppWebView() async { + return await UrlLauncherPlatform.instance.closeWebView(); +} diff --git a/packages/url_launcher/url_launcher/lib/url_launcher.dart b/packages/url_launcher/url_launcher/lib/url_launcher.dart index f28c460cce4f..36c7b60fdacd 100644 --- a/packages/url_launcher/url_launcher/lib/url_launcher.dart +++ b/packages/url_launcher/url_launcher/lib/url_launcher.dart @@ -2,150 +2,6 @@ // 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:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; - -/// Parses the specified URL string and delegates handling of it to the -/// underlying platform. -/// -/// The returned future completes with a [PlatformException] on invalid URLs and -/// schemes which cannot be handled, that is when [canLaunch] would complete -/// with false. -/// -/// By default when [forceSafariVC] is unset, the launcher -/// opens web URLs in the Safari View Controller, anything else is opened -/// using the default handler on the platform. If set to true, it opens the -/// URL in the Safari View Controller. If false, the URL is opened in the -/// default browser of the phone. Note that to work with universal links on iOS, -/// this must be set to false to let the platform's system handle the URL. -/// Set this to false if you want to use the cookies/context of the main browser -/// of the app (such as SSO flows). This setting will nullify [universalLinksOnly] -/// and will always launch a web content in the built-in Safari View Controller regardless -/// if the url is a universal link or not. -/// -/// [universalLinksOnly] is only used in iOS with iOS version >= 10.0. This setting is only validated -/// when [forceSafariVC] is set to false. The default value of this setting is false. -/// By default (when unset), the launcher will either launch the url in a browser (when the -/// url is not a universal link), or launch the respective native app content (when -/// the url is a universal link). When set to true, the launcher will only launch -/// the content if the url is a universal link and the respective app for the universal -/// link is installed on the user's device; otherwise throw a [PlatformException]. -/// -/// [forceWebView] is an Android only setting. If null or false, the URL is -/// always launched with the default browser on device. If set to true, the URL -/// is launched in a WebView. Unlike iOS, browser context is shared across -/// WebViews. -/// [enableJavaScript] is an Android only setting. If true, WebView enable -/// javascript. -/// [enableDomStorage] is an Android only setting. If true, WebView enable -/// DOM storage. -/// [headers] is an Android only setting that adds headers to the WebView. -/// When not using a WebView, the header information is passed to the browser, -/// some Android browsers do not support the [Browser.EXTRA_HEADERS](https://developer.android.com/reference/android/provider/Browser#EXTRA_HEADERS) -/// intent extra and the header information will be lost. -/// [webOnlyWindowName] is an Web only setting . _blank opens the new url in new tab , -/// _self opens the new url in current tab. -/// Default behaviour is to open the url in new tab. -/// -/// Note that if any of the above are set to true but the URL is not a web URL, -/// this will throw a [PlatformException]. -/// -/// [statusBarBrightness] Sets the status bar brightness of the application -/// after opening a link on iOS. Does nothing if no value is passed. This does -/// not handle resetting the previous status bar style. -/// -/// Returns true if launch url is successful; false is only returned when [universalLinksOnly] -/// is set to true and the universal link failed to launch. -Future launch( - String urlString, { - bool? forceSafariVC, - bool forceWebView = false, - bool enableJavaScript = false, - bool enableDomStorage = false, - bool universalLinksOnly = false, - Map headers = const {}, - Brightness? statusBarBrightness, - String? webOnlyWindowName, -}) async { - final Uri? url = Uri.tryParse(urlString.trimLeft()); - final bool isWebURL = - url != null && (url.scheme == 'http' || url.scheme == 'https'); - - if ((forceSafariVC == true || forceWebView == true) && !isWebURL) { - throw PlatformException( - code: 'NOT_A_WEB_SCHEME', - message: 'To use webview or safariVC, you need to pass' - 'in a web URL. This $urlString is not a web URL.'); - } - - /// [true] so that ui is automatically computed if [statusBarBrightness] is set. - bool previousAutomaticSystemUiAdjustment = true; - if (statusBarBrightness != null && - defaultTargetPlatform == TargetPlatform.iOS && - _ambiguate(WidgetsBinding.instance) != null) { - previousAutomaticSystemUiAdjustment = _ambiguate(WidgetsBinding.instance)! - .renderView - .automaticSystemUiAdjustment; - _ambiguate(WidgetsBinding.instance)! - .renderView - .automaticSystemUiAdjustment = false; - SystemChrome.setSystemUIOverlayStyle(statusBarBrightness == Brightness.light - ? SystemUiOverlayStyle.dark - : SystemUiOverlayStyle.light); - } - - final bool result = await UrlLauncherPlatform.instance.launch( - urlString, - useSafariVC: forceSafariVC ?? isWebURL, - useWebView: forceWebView, - enableJavaScript: enableJavaScript, - enableDomStorage: enableDomStorage, - universalLinksOnly: universalLinksOnly, - headers: headers, - webOnlyWindowName: webOnlyWindowName, - ); - - if (statusBarBrightness != null && - _ambiguate(WidgetsBinding.instance) != null) { - _ambiguate(WidgetsBinding.instance)! - .renderView - .automaticSystemUiAdjustment = previousAutomaticSystemUiAdjustment; - } - - return result; -} - -/// Checks whether the specified URL can be handled by some app installed on the -/// device. -/// -/// On some systems, such as recent versions of Android and iOS, this will -/// always return false unless the application has been configuration to allow -/// querying the system for launch support. See -/// [the README](https://pub.dev/packages/url_launcher#configuration) for -/// details. -Future canLaunch(String urlString) async { - return await UrlLauncherPlatform.instance.canLaunch(urlString); -} - -/// Closes the current WebView, if one was previously opened via a call to [launch]. -/// -/// If [launch] was never called, then this call will not have any effect. -/// -/// On Android systems, if [launch] was called without `forceWebView` being set to `true` -/// Or on IOS systems, if [launch] was called without `forceSafariVC` being set to `true`, -/// this call will not do anything either, simply because there is no -/// WebView/SafariViewController available to be closed. -Future closeWebView() async { - return await UrlLauncherPlatform.instance.closeWebView(); -} - -/// 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. -// TODO(ianh): Remove this once we roll stable in late 2021. -T? _ambiguate(T? value) => value; +export 'src/legacy_api.dart'; +export 'src/types.dart'; +export 'src/url_launcher_uri.dart'; diff --git a/packages/url_launcher/url_launcher/lib/url_launcher_string.dart b/packages/url_launcher/url_launcher/lib/url_launcher_string.dart new file mode 100644 index 000000000000..b5a12b1e39ca --- /dev/null +++ b/packages/url_launcher/url_launcher/lib/url_launcher_string.dart @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Provides a String-based alterantive to the Uri-based primary API. +// +// This is provided as a separate import because it's much easier to use +// incorrectly, so should require explicit opt-in (to avoid issues such as +// IDE auto-complete to the more error-prone APIs just by importing the +// main API). + +export 'src/types.dart'; +export 'src/url_launcher_string.dart'; diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index feb0a2c9ad95..8efda4afb580 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.0.20 +version: 6.1.6 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: @@ -34,7 +34,7 @@ dependencies: # implementations, as both are compatible. url_launcher_linux: ">=2.0.0 <4.0.0" url_launcher_macos: ">=2.0.0 <4.0.0" - url_launcher_platform_interface: ^2.0.3 + url_launcher_platform_interface: ^2.1.0 url_launcher_web: ^2.0.0 url_launcher_windows: ">=2.0.0 <4.0.0" diff --git a/packages/url_launcher/url_launcher/test/link_test.dart b/packages/url_launcher/url_launcher/test/link_test.dart index f7a98a0bf2f0..1c3d3e1e2d5b 100644 --- a/packages/url_launcher/url_launcher/test/link_test.dart +++ b/packages/url_launcher/url_launcher/test/link_test.dart @@ -9,7 +9,7 @@ import 'package:url_launcher/link.dart'; import 'package:url_launcher/src/link.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; -import 'mock_url_launcher_platform.dart'; +import 'mocks/mock_url_launcher_platform.dart'; void main() { late MockUrlLauncher mock; @@ -19,7 +19,7 @@ void main() { UrlLauncherPlatform.instance = mock; }); - group('$Link', () { + group('Link', () { testWidgets('handles null uri correctly', (WidgetTester tester) async { bool isBuilt = false; FollowLink? followLink; @@ -55,11 +55,10 @@ void main() { mock ..setLaunchExpectations( url: 'http://example.com/foobar', - useSafariVC: false, - useWebView: false, + launchMode: PreferredLaunchMode.externalApplication, universalLinksOnly: false, - enableJavaScript: false, - enableDomStorage: false, + enableJavaScript: true, + enableDomStorage: true, headers: {}, webOnlyWindowName: null, ) @@ -85,11 +84,10 @@ void main() { mock ..setLaunchExpectations( url: 'http://example.com/foobar', - useSafariVC: true, - useWebView: true, + launchMode: PreferredLaunchMode.inAppWebView, universalLinksOnly: false, - enableJavaScript: false, - enableDomStorage: false, + enableJavaScript: true, + enableDomStorage: true, headers: {}, webOnlyWindowName: null, ) diff --git a/packages/url_launcher/url_launcher/test/mock_url_launcher_platform.dart b/packages/url_launcher/url_launcher/test/mocks/mock_url_launcher_platform.dart similarity index 78% rename from packages/url_launcher/url_launcher/test/mock_url_launcher_platform.dart rename to packages/url_launcher/url_launcher/test/mocks/mock_url_launcher_platform.dart index 789c1435df80..05c8b5e4b375 100644 --- a/packages/url_launcher/url_launcher/test/mock_url_launcher_platform.dart +++ b/packages/url_launcher/url_launcher/test/mocks/mock_url_launcher_platform.dart @@ -11,6 +11,7 @@ class MockUrlLauncher extends Fake with MockPlatformInterfaceMixin implements UrlLauncherPlatform { String? url; + PreferredLaunchMode? launchMode; bool? useSafariVC; bool? useWebView; bool? enableJavaScript; @@ -25,14 +26,16 @@ class MockUrlLauncher extends Fake bool canLaunchCalled = false; bool launchCalled = false; + // ignore: use_setters_to_change_properties void setCanLaunchExpectations(String url) { this.url = url; } void setLaunchExpectations({ required String url, - required bool? useSafariVC, - required bool useWebView, + PreferredLaunchMode? launchMode, + bool? useSafariVC, + bool? useWebView, required bool enableJavaScript, required bool enableDomStorage, required bool universalLinksOnly, @@ -40,6 +43,7 @@ class MockUrlLauncher extends Fake required String? webOnlyWindowName, }) { this.url = url; + this.launchMode = launchMode; this.useSafariVC = useSafariVC; this.useWebView = useWebView; this.enableJavaScript = enableJavaScript; @@ -49,6 +53,7 @@ class MockUrlLauncher extends Fake this.webOnlyWindowName = webOnlyWindowName; } + // ignore: use_setters_to_change_properties void setResponse(bool response) { this.response = response; } @@ -86,6 +91,18 @@ class MockUrlLauncher extends Fake return response!; } + @override + Future launchUrl(String url, LaunchOptions options) async { + expect(url, this.url); + expect(options.mode, launchMode); + expect(options.webViewConfiguration.enableJavaScript, enableJavaScript); + expect(options.webViewConfiguration.enableDomStorage, enableDomStorage); + expect(options.webViewConfiguration.headers, headers); + expect(options.webOnlyWindowName, webOnlyWindowName); + launchCalled = true; + return response!; + } + @override Future closeWebView() async { closeWebViewCalled = true; diff --git a/packages/url_launcher/url_launcher/test/url_launcher_test.dart b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart similarity index 97% rename from packages/url_launcher/url_launcher/test/url_launcher_test.dart rename to packages/url_launcher/url_launcher/test/src/legacy_api_test.dart index 4e980cb37253..11d7d8f17c09 100644 --- a/packages/url_launcher/url_launcher/test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart @@ -2,16 +2,17 @@ // 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:ui'; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#105648) +// ignore: unnecessary_import +import 'dart:ui' show Brightness; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show PlatformException; import 'package:flutter_test/flutter_test.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/src/legacy_api.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; -import 'mock_url_launcher_platform.dart'; +import '../mocks/mock_url_launcher_platform.dart'; void main() { final MockUrlLauncher mock = MockUrlLauncher(); diff --git a/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart b/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart new file mode 100644 index 000000000000..0dcbc34b7dd6 --- /dev/null +++ b/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart @@ -0,0 +1,265 @@ +// 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:url_launcher/src/types.dart'; +import 'package:url_launcher/src/url_launcher_string.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +import '../mocks/mock_url_launcher_platform.dart'; + +void main() { + final MockUrlLauncher mock = MockUrlLauncher(); + UrlLauncherPlatform.instance = mock; + + group('canLaunchUrlString', () { + test('handles returning true', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setCanLaunchExpectations(urlString) + ..setResponse(true); + + final bool result = await canLaunchUrlString(urlString); + + expect(result, isTrue); + }); + + test('handles returning false', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setCanLaunchExpectations(urlString) + ..setResponse(false); + + final bool result = await canLaunchUrlString(urlString); + + expect(result, isFalse); + }); + }); + + group('launchUrlString', () { + test('default behavior with web URL', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + launchMode: PreferredLaunchMode.platformDefault, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(urlString), isTrue); + }); + + test('default behavior with non-web URL', () async { + const String urlString = 'customscheme:foo'; + mock + ..setLaunchExpectations( + url: urlString, + launchMode: PreferredLaunchMode.platformDefault, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(urlString), isTrue); + }); + + test('explicit default launch mode with web URL', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + launchMode: PreferredLaunchMode.platformDefault, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(urlString), isTrue); + }); + + test('explicit default launch mode with non-web URL', () async { + const String urlString = 'customscheme:foo'; + mock + ..setLaunchExpectations( + url: urlString, + launchMode: PreferredLaunchMode.platformDefault, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(urlString), isTrue); + }); + + test('in-app webview', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + launchMode: PreferredLaunchMode.inAppWebView, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(urlString, mode: LaunchMode.inAppWebView), + isTrue); + }); + + test('external browser', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + launchMode: PreferredLaunchMode.externalApplication, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrlString(urlString, + mode: LaunchMode.externalApplication), + isTrue); + }); + + test('external non-browser only', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + launchMode: PreferredLaunchMode.externalNonBrowserApplication, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: true, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrlString(urlString, + mode: LaunchMode.externalNonBrowserApplication), + isTrue); + }); + + test('in-app webview without javascript', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + launchMode: PreferredLaunchMode.inAppWebView, + enableJavaScript: false, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrlString(urlString, + mode: LaunchMode.inAppWebView, + webViewConfiguration: + const WebViewConfiguration(enableJavaScript: false)), + isTrue); + }); + + test('in-app webview without DOM storage', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + launchMode: PreferredLaunchMode.inAppWebView, + enableJavaScript: true, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrlString(urlString, + mode: LaunchMode.inAppWebView, + webViewConfiguration: + const WebViewConfiguration(enableDomStorage: false)), + isTrue); + }); + + test('in-app webview with headers', () async { + const String urlString = 'https://flutter.dev'; + mock + ..setLaunchExpectations( + url: urlString, + launchMode: PreferredLaunchMode.inAppWebView, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {'key': 'value'}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrlString(urlString, + mode: LaunchMode.inAppWebView, + webViewConfiguration: const WebViewConfiguration( + headers: {'key': 'value'})), + isTrue); + }); + + test('cannot launch a non-web URL in a webview', () async { + expect( + () async => await launchUrlString('tel:555-555-5555', + mode: LaunchMode.inAppWebView), + throwsA(isA())); + }); + + test('non-web URL with default options', () async { + const String emailLaunchUrlString = + 'mailto:smith@example.com?subject=Hello'; + mock + ..setLaunchExpectations( + url: emailLaunchUrlString, + launchMode: PreferredLaunchMode.platformDefault, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(emailLaunchUrlString), isTrue); + }); + + test('allows non-parsable url', () async { + // Not a valid Dart [Uri], but a valid URL on at least some platforms. + const String urlString = + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1'; + mock + ..setLaunchExpectations( + url: urlString, + launchMode: PreferredLaunchMode.platformDefault, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrlString(urlString), isTrue); + }); + }); +} diff --git a/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart b/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart new file mode 100644 index 000000000000..7685aefdd4ee --- /dev/null +++ b/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart @@ -0,0 +1,251 @@ +// 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:url_launcher/src/types.dart'; +import 'package:url_launcher/src/url_launcher_uri.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +import '../mocks/mock_url_launcher_platform.dart'; + +void main() { + final MockUrlLauncher mock = MockUrlLauncher(); + UrlLauncherPlatform.instance = mock; + + test('closeInAppWebView', () async { + await closeInAppWebView(); + expect(mock.closeWebViewCalled, isTrue); + }); + + group('canLaunchUrl', () { + test('handles returning true', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setCanLaunchExpectations(url.toString()) + ..setResponse(true); + + final bool result = await canLaunchUrl(url); + + expect(result, isTrue); + }); + + test('handles returning false', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setCanLaunchExpectations(url.toString()) + ..setResponse(false); + + final bool result = await canLaunchUrl(url); + + expect(result, isFalse); + }); + }); + + group('launchUrl', () { + test('default behavior with web URL', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + launchMode: PreferredLaunchMode.platformDefault, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrl(url), isTrue); + }); + + test('default behavior with non-web URL', () async { + final Uri url = Uri.parse('customscheme:foo'); + mock + ..setLaunchExpectations( + url: url.toString(), + launchMode: PreferredLaunchMode.platformDefault, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrl(url), isTrue); + }); + + test('explicit default launch mode with web URL', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + launchMode: PreferredLaunchMode.platformDefault, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrl(url), isTrue); + }); + + test('explicit default launch mode with non-web URL', () async { + final Uri url = Uri.parse('customscheme:foo'); + mock + ..setLaunchExpectations( + url: url.toString(), + launchMode: PreferredLaunchMode.platformDefault, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrl(url), isTrue); + }); + + test('in-app webview', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + launchMode: PreferredLaunchMode.inAppWebView, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrl(url, mode: LaunchMode.inAppWebView), isTrue); + }); + + test('external browser', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + launchMode: PreferredLaunchMode.externalApplication, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrl(url, mode: LaunchMode.externalApplication), isTrue); + }); + + test('external non-browser only', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + launchMode: PreferredLaunchMode.externalNonBrowserApplication, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: true, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrl(url, mode: LaunchMode.externalNonBrowserApplication), + isTrue); + }); + + test('in-app webview without javascript', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + launchMode: PreferredLaunchMode.inAppWebView, + enableJavaScript: false, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrl(url, + mode: LaunchMode.inAppWebView, + webViewConfiguration: + const WebViewConfiguration(enableJavaScript: false)), + isTrue); + }); + + test('in-app webview without DOM storage', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + launchMode: PreferredLaunchMode.inAppWebView, + enableJavaScript: true, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrl(url, + mode: LaunchMode.inAppWebView, + webViewConfiguration: + const WebViewConfiguration(enableDomStorage: false)), + isTrue); + }); + + test('in-app webview with headers', () async { + final Uri url = Uri.parse('https://flutter.dev'); + mock + ..setLaunchExpectations( + url: url.toString(), + launchMode: PreferredLaunchMode.inAppWebView, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {'key': 'value'}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launchUrl(url, + mode: LaunchMode.inAppWebView, + webViewConfiguration: const WebViewConfiguration( + headers: {'key': 'value'})), + isTrue); + }); + + test('cannot launch a non-web URL in a webview', () async { + expect( + () async => await launchUrl(Uri(scheme: 'tel', path: '555-555-5555'), + mode: LaunchMode.inAppWebView), + throwsA(isA())); + }); + + test('non-web URL with default options', () async { + final Uri emailLaunchUrl = Uri( + scheme: 'mailto', + path: 'smith@example.com', + queryParameters: {'subject': 'Hello'}, + ); + mock + ..setLaunchExpectations( + url: emailLaunchUrl.toString(), + launchMode: PreferredLaunchMode.platformDefault, + enableJavaScript: true, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launchUrl(emailLaunchUrl), isTrue); + }); + }); +} diff --git a/packages/url_launcher/url_launcher_android/CHANGELOG.md b/packages/url_launcher/url_launcher_android/CHANGELOG.md index 9ec1f65911c6..934d8da556b7 100644 --- a/packages/url_launcher/url_launcher_android/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_android/CHANGELOG.md @@ -1,3 +1,30 @@ +## 6.0.21 + +* Updates androidx.annotation to 1.2.0. + +## 6.0.20 + +* Updates android gradle plugin to 4.2.0. + +## 6.0.19 + +* Revert gradle back to 3.4.2. + +## 6.0.18 + +* Updates gradle to 7.2.2. +* Updates minimum Flutter version to 2.10. + +## 6.0.17 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 6.0.16 + +* Adds fallback querying for `canLaunch` with web URLs, to avoid false negatives + when there is a custom scheme handler. + ## 6.0.15 * Switches to an in-package method channel implementation. diff --git a/packages/url_launcher/url_launcher_android/android/build.gradle b/packages/url_launcher/url_launcher_android/android/build.gradle index 180d7b2bdd9c..dbd68d99c1a2 100644 --- a/packages/url_launcher/url_launcher_android/android/build.gradle +++ b/packages/url_launcher/url_launcher_android/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.android.tools.build:gradle:4.2.0' } } @@ -22,13 +22,14 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 31 + compileSdkVersion 33 defaultConfig { minSdkVersion 16 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { + disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' } @@ -48,9 +49,9 @@ android { } dependencies { - compileOnly 'androidx.annotation:annotation:1.0.0' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:1.10.19' + compileOnly 'androidx.annotation:annotation:1.2.0' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:4.8.0' testImplementation 'androidx.test:core:1.0.0' testImplementation 'org.robolectric:robolectric:4.3' } diff --git a/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java b/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java index b60192531dbd..6bd88b650802 100644 --- a/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java +++ b/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java @@ -4,8 +4,8 @@ package io.flutter.plugins.urllauncher; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; diff --git a/packages/url_launcher/url_launcher_android/example/README.md b/packages/url_launcher/url_launcher_android/example/README.md index c200da8974d1..96b8bb17dbff 100644 --- a/packages/url_launcher/url_launcher_android/example/README.md +++ b/packages/url_launcher/url_launcher_android/example/README.md @@ -1,8 +1,9 @@ -# url_launcher_example +# Platform Implementation Test App -Demonstrates how to use the url_launcher plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/url_launcher/url_launcher_android/example/android/app/build.gradle b/packages/url_launcher/url_launcher_android/example/android/app/build.gradle index ca41cb14b6f4..8c7e84563ee6 100644 --- a/packages/url_launcher/url_launcher_android/example/android/app/build.gradle +++ b/packages/url_launcher/url_launcher_android/example/android/app/build.gradle @@ -54,7 +54,7 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } diff --git a/packages/url_launcher/url_launcher_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/url_launcher/url_launcher_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/url_launcher/url_launcher_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/url_launcher/url_launcher_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/url_launcher/url_launcher_android/example/android/build.gradle b/packages/url_launcher/url_launcher_android/example/android/build.gradle index 328175bb6ac5..c21bff8e0a2f 100644 --- a/packages/url_launcher/url_launcher_android/example/android/build.gradle +++ b/packages/url_launcher/url_launcher_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.2.1' + classpath 'com.android.tools.build:gradle:7.0.1' } } diff --git a/packages/url_launcher/url_launcher_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/url_launcher/url_launcher_android/example/android/gradle/wrapper/gradle-wrapper.properties index 4ae10e927b38..e7c709db2454 100644 --- a/packages/url_launcher/url_launcher_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/url_launcher/url_launcher_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-6.7.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip 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 8721c587075e..672ae4a27665 100644 --- a/packages/url_launcher/url_launcher_android/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_android/example/lib/main.dart @@ -10,10 +10,12 @@ import 'package:flutter/material.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -31,77 +33,78 @@ class MyHomePage extends StatefulWidget { final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { + final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; + bool _hasCallSupport = false; Future? _launched; String _phone = ''; + @override + void initState() { + super.initState(); + // Check for phone call support. + launcher.canLaunch('tel:123').then((bool result) { + setState(() { + _hasCallSupport = result; + }); + }); + } + Future _launchInBrowser(String url) async { - final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; - if (await launcher.canLaunch(url)) { - await launcher.launch( - url, - useSafariVC: false, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {'my_header_key': 'my_header_value'}, - ); - } else { + if (!await launcher.launch( + url, + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + )) { throw 'Could not launch $url'; } } - Future _launchInWebViewOrVC(String url) async { - final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; - if (await launcher.canLaunch(url)) { - await launcher.launch( - url, - useSafariVC: true, - useWebView: true, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {'my_header_key': 'my_header_value'}, - ); - } else { + Future _launchInWebView(String url) async { + if (!await launcher.launch( + url, + useSafariVC: true, + useWebView: true, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {'my_header_key': 'my_header_value'}, + )) { throw 'Could not launch $url'; } } Future _launchInWebViewWithJavaScript(String url) async { - final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; - if (await launcher.canLaunch(url)) { - await launcher.launch( - url, - useSafariVC: true, - useWebView: true, - enableJavaScript: true, - enableDomStorage: false, - universalLinksOnly: false, - headers: {}, - ); - } else { + if (!await launcher.launch( + url, + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + )) { throw 'Could not launch $url'; } } Future _launchInWebViewWithDomStorage(String url) async { - final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; - if (await launcher.canLaunch(url)) { - await launcher.launch( - url, - useSafariVC: true, - useWebView: true, - enableJavaScript: false, - enableDomStorage: true, - universalLinksOnly: false, - headers: {}, - ); - } else { + if (!await launcher.launch( + url, + useSafariVC: true, + useWebView: true, + enableJavaScript: false, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + )) { throw 'Could not launch $url'; } } @@ -114,25 +117,30 @@ class _MyHomePageState extends State { } } - Future _makePhoneCall(String url) async { - final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; - if (await launcher.canLaunch(url)) { - await launcher.launch( - url, - useSafariVC: false, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: true, - headers: {}, - ); - } else { - throw 'Could not launch $url'; - } + Future _makePhoneCall(String phoneNumber) async { + // Use `Uri` to ensure that `phoneNumber` is properly URL-encoded. + // Just using 'tel:$phoneNumber' would create invalid URLs in some cases, + // such as spaces in the input, which would cause `launch` to fail on some + // platforms. + final Uri launchUri = Uri( + scheme: 'tel', + path: phoneNumber, + ); + await launcher.launch( + launchUri.toString(), + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: true, + headers: {}, + ); } @override Widget build(BuildContext context) { + // onPressed calls using this URL are not gated on a 'canLaunch' check + // because the assumption is that every device can launch a web URL. const String toLaunch = 'https://www.cylog.org/headers/'; return Scaffold( appBar: AppBar( @@ -151,10 +159,14 @@ class _MyHomePageState extends State { hintText: 'Input the phone number to launch')), ), ElevatedButton( - onPressed: () => setState(() { - _launched = _makePhoneCall('tel:$_phone'); - }), - child: const Text('Make phone call'), + onPressed: _hasCallSupport + ? () => setState(() { + _launched = _makePhoneCall(_phone); + }) + : null, + child: _hasCallSupport + ? const Text('Make phone call') + : const Text('Calling not supported'), ), const Padding( padding: EdgeInsets.all(16.0), @@ -169,7 +181,7 @@ class _MyHomePageState extends State { const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInWebViewOrVC(toLaunch); + _launched = _launchInWebView(toLaunch); }), child: const Text('Launch in app'), ), @@ -177,21 +189,21 @@ class _MyHomePageState extends State { onPressed: () => setState(() { _launched = _launchInWebViewWithJavaScript(toLaunch); }), - child: const Text('Launch in app(JavaScript ON)'), + child: const Text('Launch in app (JavaScript ON)'), ), ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewWithDomStorage(toLaunch); }), - child: const Text('Launch in app(DOM storage ON)'), + child: const Text('Launch in app (DOM storage ON)'), ), const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInWebViewOrVC(toLaunch); + _launched = _launchInWebView(toLaunch); Timer(const Duration(seconds: 5), () { print('Closing WebView after 5 seconds...'); - UrlLauncherPlatform.instance.closeWebView(); + launcher.closeWebView(); }); }), child: const Text('Launch in app + close after 5 seconds'), diff --git a/packages/url_launcher/url_launcher_android/example/pubspec.yaml b/packages/url_launcher/url_launcher_android/example/pubspec.yaml index 9af7b2876da9..6c922c7a0f7d 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.5.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -16,10 +16,13 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ + url_launcher_platform_interface: ^2.0.3 dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter mockito: ^5.0.0 diff --git a/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart index 52c46356489d..1aa093a36451 100644 --- a/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart +++ b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart @@ -22,7 +22,24 @@ class UrlLauncherAndroid extends UrlLauncherPlatform { final LinkDelegate? linkDelegate = null; @override - Future canLaunch(String url) { + Future canLaunch(String url) async { + final bool canLaunchSpecificUrl = await _canLaunchUrl(url); + if (!canLaunchSpecificUrl) { + final String scheme = _getUrlScheme(url); + // canLaunch can return false when a custom application is registered to + // handle a web URL, but the caller doesn't have permission to see what + // that handler is. If that happens, try a web URL (with the same scheme + // variant, to be safe) that should not have a custom handler. If that + // returns true, then there is a browser, which means that there is + // at least one handler for the original URL. + if (scheme == 'http' || scheme == 'https') { + return await _canLaunchUrl('$scheme://flutter.dev'); + } + } + return canLaunchSpecificUrl; + } + + Future _canLaunchUrl(String url) { return _channel.invokeMethod( 'canLaunch', {'url': url}, @@ -57,4 +74,16 @@ class UrlLauncherAndroid extends UrlLauncherPlatform { }, ).then((bool? value) => value ?? false); } + + // Returns the part of [url] up to the first ':', or an empty string if there + // is no ':'. This deliberately does not use [Uri] to extract the scheme + // so that it works on strings that aren't actually valid URLs, since Android + // is very lenient about what it accepts for launching. + String _getUrlScheme(String url) { + final int schemeEnd = url.indexOf(':'); + if (schemeEnd == -1) { + return ''; + } + return url.substring(0, schemeEnd); + } } diff --git a/packages/url_launcher/url_launcher_android/pubspec.yaml b/packages/url_launcher/url_launcher_android/pubspec.yaml index b8706aeb13f2..e97fde31b2e0 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.15 +version: 6.0.21 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.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 909d2c100ecf..eebd8cd4c059 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 @@ -10,10 +10,12 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('$UrlLauncherAndroid', () { - const MethodChannel channel = - MethodChannel('plugins.flutter.io/url_launcher_android'); - final List log = []; + const MethodChannel channel = + MethodChannel('plugins.flutter.io/url_launcher_android'); + late List log; + + setUp(() { + log = []; channel.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); @@ -21,19 +23,21 @@ void main() { // returned by the method channel if no return statement is specified. return null; }); + }); - tearDown(() { - log.clear(); - }); - - test('registers instance', () { - UrlLauncherAndroid.registerWith(); - expect(UrlLauncherPlatform.instance, isA()); - }); + test('registers instance', () { + UrlLauncherAndroid.registerWith(); + expect(UrlLauncherPlatform.instance, isA()); + }); - test('canLaunch', () async { + group('canLaunch', () { + test('calls through', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return true; + }); final UrlLauncherAndroid launcher = UrlLauncherAndroid(); - await launcher.canLaunch('http://example.com/'); + final bool canLaunch = await launcher.canLaunch('http://example.com/'); expect( log, [ @@ -42,16 +46,64 @@ void main() { }) ], ); + expect(canLaunch, true); }); - test('canLaunch should return false if platform returns null', () async { + test('returns false if platform returns null', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); final bool canLaunch = await launcher.canLaunch('http://example.com/'); expect(canLaunch, false); }); - test('launch', () async { + 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 { + log.add(methodCall); + return methodCall.arguments['url'] != specificUrl; + }); + + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + final bool canLaunch = await launcher.canLaunch(specificUrl); + + expect(canLaunch, true); + expect(log.length, 2); + expect(log[1].arguments['url'], genericUrl); + }); + + 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 { + log.add(methodCall); + return methodCall.arguments['url'] != specificUrl; + }); + + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + final bool canLaunch = await launcher.canLaunch(specificUrl); + + expect(canLaunch, true); + expect(log.length, 2); + expect(log[1].arguments['url'], genericUrl); + }); + + test('does not a generic URL if a non-web URL returns false', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return false; + }); + + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + final bool canLaunch = await launcher.canLaunch('sms:12345'); + + expect(canLaunch, false); + expect(log.length, 1); + }); + }); + + group('launch', () { + test('calls through', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.launch( 'http://example.com/', @@ -77,7 +129,7 @@ void main() { ); }); - test('launch with headers', () async { + test('passes headers', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.launch( 'http://example.com/', @@ -103,7 +155,7 @@ void main() { ); }); - test('launch universal links only', () async { + test('handles universal links only', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.launch( 'http://example.com/', @@ -129,7 +181,7 @@ void main() { ); }); - test('launch force WebView', () async { + test('handles force WebView', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.launch( 'http://example.com/', @@ -155,7 +207,7 @@ void main() { ); }); - test('launch force WebView enable javascript', () async { + test('handles force WebView with javascript', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.launch( 'http://example.com/', @@ -181,7 +233,7 @@ void main() { ); }); - test('launch force WebView enable DOM storage', () async { + test('handles force WebView with DOM storage', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.launch( 'http://example.com/', @@ -207,7 +259,7 @@ void main() { ); }); - test('launch should return false if platform returns null', () async { + test('returns false if platform returns null', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); final bool launched = await launcher.launch( 'http://example.com/', @@ -221,8 +273,10 @@ void main() { expect(launched, false); }); + }); - test('closeWebView default behavior', () async { + group('closeWebView', () { + test('calls through', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(); await launcher.closeWebView(); expect( diff --git a/packages/url_launcher/url_launcher_ios/CHANGELOG.md b/packages/url_launcher/url_launcher_ios/CHANGELOG.md index 6e0c8d6a20d7..cf018da4f59d 100644 --- a/packages/url_launcher/url_launcher_ios/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_ios/CHANGELOG.md @@ -1,3 +1,16 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 6.0.17 + +* Suppresses warnings for pre-iOS-13 codepaths. + +## 6.0.16 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 6.0.15 * Switches to an in-package method channel implementation. diff --git a/packages/url_launcher/url_launcher_ios/example/README.md b/packages/url_launcher/url_launcher_ios/example/README.md index c200da8974d1..96b8bb17dbff 100644 --- a/packages/url_launcher/url_launcher_ios/example/README.md +++ b/packages/url_launcher/url_launcher_ios/example/README.md @@ -1,8 +1,9 @@ -# url_launcher_example +# Platform Implementation Test App -Demonstrates how to use the url_launcher plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. 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 2f73622ebb41..7aa3a4b74e83 100644 --- a/packages/url_launcher/url_launcher_ios/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_ios/example/lib/main.dart @@ -10,10 +10,12 @@ import 'package:flutter/material.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -31,7 +33,7 @@ class MyHomePage extends StatefulWidget { final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { diff --git a/packages/url_launcher/url_launcher_ios/example/pubspec.yaml b/packages/url_launcher/url_launcher_ios/example/pubspec.yaml index da4c72cd13bb..9a134c747fa4 100644 --- a/packages/url_launcher/url_launcher_ios/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_ios/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -16,10 +16,13 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ + url_launcher_platform_interface: ^2.0.3 dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter mockito: ^5.0.0 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 1aceedc8b1de..af720c87b8b2 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m @@ -136,8 +136,13 @@ - (void)closeWebViewWithResult:(FlutterResult)result API_AVAILABLE(ios(9.0)) { } - (UIViewController *)topViewController { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // TODO(stuartmorgan) Provide a non-deprecated codepath. See + // https://github.com/flutter/flutter/issues/104117 return [self topViewControllerFromViewController:[UIApplication sharedApplication] .keyWindow.rootViewController]; +#pragma clang diagnostic pop } /** 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 e22ba3db9854..1c0e81964252 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 @@ -11,7 +11,7 @@ A Flutter plugin for making the underlying platform (Android or iOS) launch a UR s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/url_launcher' s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_ios' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_ios' } s.documentation_url = 'https://pub.dev/packages/url_launcher' s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' diff --git a/packages/url_launcher/url_launcher_ios/pubspec.yaml b/packages/url_launcher/url_launcher_ios/pubspec.yaml index 8a5bfd20c8f4..5e06a80b4cfe 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.15 +version: 6.0.17 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_linux/CHANGELOG.md b/packages/url_launcher/url_launcher_linux/CHANGELOG.md index 0fc373f2ebb1..69d8b24ddf6f 100644 --- a/packages/url_launcher/url_launcher_linux/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 3.0.1 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 3.0.0 * Changes the major version since, due to a typo in `default_package` in diff --git a/packages/url_launcher/url_launcher_linux/example/README.md b/packages/url_launcher/url_launcher_linux/example/README.md index c200da8974d1..96b8bb17dbff 100644 --- a/packages/url_launcher/url_launcher_linux/example/README.md +++ b/packages/url_launcher/url_launcher_linux/example/README.md @@ -1,8 +1,9 @@ -# url_launcher_example +# Platform Implementation Test App -Demonstrates how to use the url_launcher plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. 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 a9a5d22dadd5..0b985e78ac0d 100644 --- a/packages/url_launcher/url_launcher_linux/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_linux/example/lib/main.dart @@ -9,10 +9,12 @@ import 'package:flutter/material.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -30,7 +32,7 @@ class MyHomePage extends StatefulWidget { final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { diff --git a/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugins.cmake b/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugins.cmake index 1fc8ed344297..f16b4c34213a 100644 --- a/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugins.cmake +++ b/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST url_launcher_linux ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml index 5e6c3fc5384f..17effeb1ffcb 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: ">=1.20.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -21,6 +21,8 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml index cb9a0be0aa41..99b237506c60 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.0 +version: 3.0.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_macos/CHANGELOG.md b/packages/url_launcher/url_launcher_macos/CHANGELOG.md index 082bc45fc2e8..7386ecced865 100644 --- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 3.0.1 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 3.0.0 * Changes the major version since, due to a typo in `default_package` in diff --git a/packages/url_launcher/url_launcher_macos/example/README.md b/packages/url_launcher/url_launcher_macos/example/README.md index c200da8974d1..96b8bb17dbff 100644 --- a/packages/url_launcher/url_launcher_macos/example/README.md +++ b/packages/url_launcher/url_launcher_macos/example/README.md @@ -1,8 +1,9 @@ -# url_launcher_example +# Platform Implementation Test App -Demonstrates how to use the url_launcher plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. 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 a9a5d22dadd5..0b985e78ac0d 100644 --- a/packages/url_launcher/url_launcher_macos/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_macos/example/lib/main.dart @@ -9,10 +9,12 @@ import 'package:flutter/material.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -30,7 +32,7 @@ class MyHomePage extends StatefulWidget { final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { diff --git a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml index 9bc3062dd08f..3b802ea229ba 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: ">=1.20.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -21,6 +21,8 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/url_launcher/url_launcher_macos/macos/url_launcher_macos.podspec b/packages/url_launcher/url_launcher_macos/macos/url_launcher_macos.podspec index 408df1f9ef45..270adc60b81f 100644 --- a/packages/url_launcher/url_launcher_macos/macos/url_launcher_macos.podspec +++ b/packages/url_launcher/url_launcher_macos/macos/url_launcher_macos.podspec @@ -11,7 +11,7 @@ Pod::Spec.new do |s| s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_macos' s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_macos' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_macos' } s.source_files = 'Classes/**/*' s.dependency 'FlutterMacOS' diff --git a/packages/url_launcher/url_launcher_macos/pubspec.yaml b/packages/url_launcher/url_launcher_macos/pubspec.yaml index 8b5183b1914d..eaf210a367b6 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.0 +version: 3.0.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md index 1a9c575c27cb..d45ca36e3906 100644 --- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.1.1 + +* Updates imports for `prefer_relative_imports`. +* Updates minimum Flutter version to 2.10. + +## 2.1.0 + +* Adds a new `launchUrl` method corresponding to the new app-facing interface. + ## 2.0.5 * Updates code for new analysis options. diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/src/types.dart b/packages/url_launcher/url_launcher_platform_interface/lib/src/types.dart new file mode 100644 index 000000000000..08d87e03a128 --- /dev/null +++ b/packages/url_launcher/url_launcher_platform_interface/lib/src/types.dart @@ -0,0 +1,69 @@ +// 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'; + +/// The desired mode to launch a URL. +/// +/// Support for these modes varies by platform. Platforms that do not support +/// the requested mode may substitute another mode. +enum PreferredLaunchMode { + /// Leaves the decision of how to launch the URL to the platform + /// implementation. + platformDefault, + + /// Loads the URL in an in-app web view (e.g., Safari View Controller). + inAppWebView, + + /// Passes the URL to the OS to be handled by another application. + externalApplication, + + /// Passes the URL to the OS to be handled by another non-browser application. + externalNonBrowserApplication, +} + +/// Additional configuration options for [PreferredLaunchMode.inAppWebView]. +/// +/// Not all options are supported on all platforms. This is a superset of +/// available options exposed across all implementations. +@immutable +class InAppWebViewConfiguration { + /// Creates a new WebViewConfiguration with the given settings. + const InAppWebViewConfiguration({ + this.enableJavaScript = true, + this.enableDomStorage = true, + this.headers = const {}, + }); + + /// Whether or not JavaScript is enabled for the web content. + final bool enableJavaScript; + + /// Whether or not DOM storage is enabled for the web content. + final bool enableDomStorage; + + /// Additional headers to pass in the load request. + final Map headers; +} + +/// Options for [launchUrl]. +@immutable +class LaunchOptions { + /// Creates a new parameter object with the given options. + const LaunchOptions({ + this.mode = PreferredLaunchMode.platformDefault, + this.webViewConfiguration = const InAppWebViewConfiguration(), + this.webOnlyWindowName, + }); + + /// The requested launch mode. + final PreferredLaunchMode mode; + + /// Configuration for the web view in [PreferredLaunchMode.inAppWebView] mode. + final InAppWebViewConfiguration webViewConfiguration; + + /// A web-platform-specific option to set the link target. + /// + /// Default behaviour when unset should be to open the url in a new tab. + final String? webOnlyWindowName; +} diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/src/url_launcher_platform.dart b/packages/url_launcher/url_launcher_platform_interface/lib/src/url_launcher_platform.dart new file mode 100644 index 000000000000..8928d4249e90 --- /dev/null +++ b/packages/url_launcher/url_launcher_platform_interface/lib/src/url_launcher_platform.dart @@ -0,0 +1,94 @@ +// 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:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../link.dart'; +import '../method_channel_url_launcher.dart'; +import '../url_launcher_platform_interface.dart'; + +/// The interface that implementations of url_launcher must implement. +/// +/// Platform implementations should extend this class rather than implement it as `url_launcher` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [UrlLauncherPlatform] methods. +abstract class UrlLauncherPlatform extends PlatformInterface { + /// Constructs a UrlLauncherPlatform. + UrlLauncherPlatform() : super(token: _token); + + static final Object _token = Object(); + + static UrlLauncherPlatform _instance = MethodChannelUrlLauncher(); + + /// The default instance of [UrlLauncherPlatform] to use. + /// + /// Defaults to [MethodChannelUrlLauncher]. + static UrlLauncherPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [UrlLauncherPlatform] when they register themselves. + // TODO(amirh): Extract common platform interface logic. + // https://github.com/flutter/flutter/issues/43368 + static set instance(UrlLauncherPlatform instance) { + PlatformInterface.verify(instance, _token); + _instance = instance; + } + + /// The delegate used by the Link widget to build itself. + LinkDelegate? get linkDelegate; + + /// Returns `true` if this platform is able to launch [url]. + Future canLaunch(String url) { + throw UnimplementedError('canLaunch() has not been implemented.'); + } + + /// Passes [url] to the underlying platform for handling. + /// + /// Returns `true` if the given [url] was successfully launched. + /// + /// For documentation on the other arguments, see the `launch` documentation + /// in `package:url_launcher/url_launcher.dart`. + Future launch( + String url, { + required bool useSafariVC, + required bool useWebView, + required bool enableJavaScript, + required bool enableDomStorage, + required bool universalLinksOnly, + required Map headers, + String? webOnlyWindowName, + }) { + throw UnimplementedError('launch() has not been implemented.'); + } + + /// Passes [url] to the underlying platform for handling. + /// + /// Returns `true` if the given [url] was successfully launched. + Future launchUrl(String url, LaunchOptions options) { + final bool isWebURL = url.startsWith('http:') || url.startsWith('https:'); + final bool useWebView = options.mode == PreferredLaunchMode.inAppWebView || + (isWebURL && options.mode == PreferredLaunchMode.platformDefault); + + return launch( + url, + useSafariVC: useWebView, + useWebView: useWebView, + enableJavaScript: options.webViewConfiguration.enableJavaScript, + enableDomStorage: options.webViewConfiguration.enableDomStorage, + universalLinksOnly: + options.mode == PreferredLaunchMode.externalNonBrowserApplication, + headers: options.webViewConfiguration.headers, + webOnlyWindowName: options.webOnlyWindowName, + ); + } + + /// Closes the WebView, if one was opened earlier by [launch]. + Future closeWebView() { + throw UnimplementedError('closeWebView() has not been implemented.'); + } +} diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart index 18d64eff8dcb..3312c2f5cd28 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart @@ -2,69 +2,5 @@ // 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:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:url_launcher_platform_interface/link.dart'; - -import 'method_channel_url_launcher.dart'; - -/// The interface that implementations of url_launcher must implement. -/// -/// Platform implementations should extend this class rather than implement it as `url_launcher` -/// does not consider newly added methods to be breaking changes. Extending this class -/// (using `extends`) ensures that the subclass will get the default implementation, while -/// platform implementations that `implements` this interface will be broken by newly added -/// [UrlLauncherPlatform] methods. -abstract class UrlLauncherPlatform extends PlatformInterface { - /// Constructs a UrlLauncherPlatform. - UrlLauncherPlatform() : super(token: _token); - - static final Object _token = Object(); - - static UrlLauncherPlatform _instance = MethodChannelUrlLauncher(); - - /// The default instance of [UrlLauncherPlatform] to use. - /// - /// Defaults to [MethodChannelUrlLauncher]. - static UrlLauncherPlatform get instance => _instance; - - /// Platform-specific plugins should set this with their own platform-specific - /// class that extends [UrlLauncherPlatform] when they register themselves. - // TODO(amirh): Extract common platform interface logic. - // https://github.com/flutter/flutter/issues/43368 - static set instance(UrlLauncherPlatform instance) { - PlatformInterface.verify(instance, _token); - _instance = instance; - } - - /// The delegate used by the Link widget to build itself. - LinkDelegate? get linkDelegate; - - /// Returns `true` if this platform is able to launch [url]. - Future canLaunch(String url) { - throw UnimplementedError('canLaunch() has not been implemented.'); - } - - /// Returns `true` if the given [url] was successfully launched. - /// - /// For documentation on the other arguments, see the `launch` documentation - /// in `package:url_launcher/url_launcher.dart`. - Future launch( - String url, { - required bool useSafariVC, - required bool useWebView, - required bool enableJavaScript, - required bool enableDomStorage, - required bool universalLinksOnly, - required Map headers, - String? webOnlyWindowName, - }) { - throw UnimplementedError('launch() has not been implemented.'); - } - - /// Closes the WebView, if one was opened earlier by [launch]. - Future closeWebView() { - throw UnimplementedError('closeWebView() has not been implemented.'); - } -} +export 'src/types.dart'; +export 'src/url_launcher_platform.dart'; diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml index cdbdfefba93a..4364e116c508 100644 --- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml +++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/u issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.0.5 +version: 2.1.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.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 e44e80bab02c..c8ec08c53095 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 @@ -24,7 +24,14 @@ void main() { test('Cannot be implemented with `implements`', () { expect(() { UrlLauncherPlatform.instance = ImplementsUrlLauncherPlatform(); - }, throwsA(isInstanceOf())); + // In versions of `package:plugin_platform_interface` prior to fixing + // https://github.com/flutter/flutter/issues/109339, an attempt to + // implement a platform interface using `implements` would sometimes + // throw a `NoSuchMethodError` and other times throw an + // `AssertionError`. After the issue is fixed, an `AssertionError` will + // always be thrown. For the purpose of this test, we don't really care + // what exception is thrown, so just allow any exception. + }, throwsA(anything)); }); test('Can be mocked with `implements`', () { diff --git a/packages/url_launcher/url_launcher_platform_interface/test/url_launcher_platform_test.dart b/packages/url_launcher/url_launcher_platform_interface/test/url_launcher_platform_test.dart new file mode 100644 index 000000000000..f764f679f96d --- /dev/null +++ b/packages/url_launcher/url_launcher_platform_interface/test/url_launcher_platform_test.dart @@ -0,0 +1,121 @@ +// 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:url_launcher_platform_interface/link.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +class CapturingUrlLauncher extends UrlLauncherPlatform { + String? url; + bool? useSafariVC; + bool? useWebView; + bool? enableJavaScript; + bool? enableDomStorage; + bool? universalLinksOnly; + Map headers = {}; + String? webOnlyWindowName; + + @override + final LinkDelegate? linkDelegate = null; + + @override + Future launch( + String url, { + required bool useSafariVC, + required bool useWebView, + required bool enableJavaScript, + required bool enableDomStorage, + required bool universalLinksOnly, + required Map headers, + String? webOnlyWindowName, + }) async { + this.url = url; + this.useSafariVC = useSafariVC; + this.useWebView = useWebView; + this.enableJavaScript = enableJavaScript; + this.enableDomStorage = enableDomStorage; + this.universalLinksOnly = universalLinksOnly; + this.headers = headers; + this.webOnlyWindowName = webOnlyWindowName; + + return true; + } +} + +void main() { + test('launchUrl calls through to launch with default options for web URL', + () async { + final CapturingUrlLauncher launcher = CapturingUrlLauncher(); + + await launcher.launchUrl('https://flutter.dev', const LaunchOptions()); + + expect(launcher.url, 'https://flutter.dev'); + expect(launcher.useSafariVC, true); + expect(launcher.useWebView, true); + expect(launcher.enableJavaScript, true); + expect(launcher.enableDomStorage, true); + expect(launcher.universalLinksOnly, false); + expect(launcher.headers, isEmpty); + expect(launcher.webOnlyWindowName, null); + }); + + test('launchUrl calls through to launch with default options for non-web URL', + () async { + final CapturingUrlLauncher launcher = CapturingUrlLauncher(); + + await launcher.launchUrl('tel:123456789', const LaunchOptions()); + + expect(launcher.url, 'tel:123456789'); + expect(launcher.useSafariVC, false); + expect(launcher.useWebView, false); + expect(launcher.enableJavaScript, true); + expect(launcher.enableDomStorage, true); + expect(launcher.universalLinksOnly, false); + expect(launcher.headers, isEmpty); + expect(launcher.webOnlyWindowName, null); + }); + + test('launchUrl calls through to launch with universal links', () async { + final CapturingUrlLauncher launcher = CapturingUrlLauncher(); + + await launcher.launchUrl( + 'https://flutter.dev', + const LaunchOptions( + mode: PreferredLaunchMode.externalNonBrowserApplication)); + + expect(launcher.url, 'https://flutter.dev'); + expect(launcher.useSafariVC, false); + expect(launcher.useWebView, false); + expect(launcher.enableJavaScript, true); + expect(launcher.enableDomStorage, true); + expect(launcher.universalLinksOnly, true); + expect(launcher.headers, isEmpty); + expect(launcher.webOnlyWindowName, null); + }); + + test('launchUrl calls through to launch with all non-default options', + () async { + final CapturingUrlLauncher launcher = CapturingUrlLauncher(); + + await launcher.launchUrl( + 'https://flutter.dev', + const LaunchOptions( + mode: PreferredLaunchMode.externalApplication, + webViewConfiguration: InAppWebViewConfiguration( + enableJavaScript: false, + enableDomStorage: false, + headers: {'foo': 'bar'}), + webOnlyWindowName: 'a_name', + )); + + expect(launcher.url, 'https://flutter.dev'); + expect(launcher.useSafariVC, false); + expect(launcher.useWebView, false); + expect(launcher.enableJavaScript, false); + expect(launcher.enableDomStorage, false); + expect(launcher.universalLinksOnly, false); + expect(launcher.headers['foo'], 'bar'); + expect(launcher.webOnlyWindowName, 'a_name'); + }); +} diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index a434b7af70c2..5454338bde51 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,3 +1,27 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 2.0.13 + +* Updates `url_launcher_platform_interface` constraint to the correct minimum + version. + +## 2.0.12 + +* Fixes call to `setState` after dispose on the `Link` widget. +[Issue](https://github.com/flutter/flutter/issues/102741). +* Removes unused `BuildContext` from the `LinkViewController`. + +## 2.0.11 + +* Minor fixes for new analysis options. + +## 2.0.10 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 2.0.9 - Fixes invalid routes when opening a `Link` in a new tab diff --git a/packages/url_launcher/url_launcher_web/example/README.md b/packages/url_launcher/url_launcher_web/example/README.md index 3cdecfab2ab9..0e51ae5ecbd2 100644 --- a/packages/url_launcher/url_launcher_web/example/README.md +++ b/packages/url_launcher/url_launcher_web/example/README.md @@ -1,4 +1,14 @@ -# Testing +# 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. + +## Testing This package uses `package:integration_test` to run its tests in a web browser. @@ -7,6 +17,3 @@ in the Flutter wiki for instructions to setup and run the tests in this package. Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. - -See [Plugin Tests > Web Tests > Mocks](https://github.com/flutter/flutter/wiki/Plugin-Tests#mocks) -in the Flutter wiki for more information about the `.mocks.dart` files in this package. \ No newline at end of file diff --git a/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart b/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart index 3b75e0556686..6b19861c5ce5 100644 --- a/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart +++ b/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart @@ -123,6 +123,36 @@ void main() { final html.Element anchor = _findSingleAnchor(); expect(anchor.hasAttribute('href'), false); }); + + testWidgets('can be created and disposed', (WidgetTester tester) async { + final Uri uri = Uri.parse('http://foobar'); + const int itemCount = 500; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: ListView.builder( + itemCount: itemCount, + itemBuilder: (_, int index) => WebLinkDelegate(TestLinkInfo( + uri: uri, + target: LinkTarget.defaultTarget, + builder: (BuildContext context, FollowLink? followLink) => + Text('#$index', textAlign: TextAlign.center), + )), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + await tester.scrollUntilVisible( + find.text('#${itemCount - 1}'), + 2500, + maxScrolls: 1000, + ); + }); }); } diff --git a/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart index 36903b0a4250..0717dc7ff478 100644 --- a/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart +++ b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart @@ -1,13 +1,15 @@ -// Mocks generated by Mockito 5.0.17 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in regular_integration_tests/integration_test/url_launcher_web_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; import 'dart:html' as _i2; import 'dart:math' as _i4; 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 @@ -16,40 +18,170 @@ import 'package:mockito/mockito.dart' as _i1; // 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 _FakeDocument_0 extends _i1.Fake implements _i2.Document {} +class _FakeDocument_0 extends _i1.SmartFake implements _i2.Document { + _FakeDocument_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeLocation_1 extends _i1.Fake implements _i2.Location {} +class _FakeLocation_1 extends _i1.SmartFake implements _i2.Location { + _FakeLocation_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeConsole_2 extends _i1.Fake implements _i2.Console {} +class _FakeConsole_2 extends _i1.SmartFake implements _i2.Console { + _FakeConsole_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeHistory_3 extends _i1.Fake implements _i2.History {} +class _FakeHistory_3 extends _i1.SmartFake implements _i2.History { + _FakeHistory_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeStorage_4 extends _i1.Fake implements _i2.Storage {} +class _FakeStorage_4 extends _i1.SmartFake implements _i2.Storage { + _FakeStorage_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeNavigator_5 extends _i1.Fake implements _i2.Navigator {} +class _FakeNavigator_5 extends _i1.SmartFake implements _i2.Navigator { + _FakeNavigator_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakePerformance_6 extends _i1.Fake implements _i2.Performance {} +class _FakePerformance_6 extends _i1.SmartFake implements _i2.Performance { + _FakePerformance_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeEvents_7 extends _i1.Fake implements _i2.Events {} +class _FakeEvents_7 extends _i1.SmartFake implements _i2.Events { + _FakeEvents_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeWindowBase_8 extends _i1.Fake implements _i2.WindowBase {} +class _FakeWindowBase_8 extends _i1.SmartFake implements _i2.WindowBase { + _FakeWindowBase_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeFileSystem_9 extends _i1.Fake implements _i2.FileSystem {} +class _FakeFileSystem_9 extends _i1.SmartFake implements _i2.FileSystem { + _FakeFileSystem_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeStylePropertyMapReadonly_10 extends _i1.Fake - implements _i2.StylePropertyMapReadonly {} +class _FakeStylePropertyMapReadonly_10 extends _i1.SmartFake + implements _i2.StylePropertyMapReadonly { + _FakeStylePropertyMapReadonly_10( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeMediaQueryList_11 extends _i1.Fake implements _i2.MediaQueryList {} +class _FakeMediaQueryList_11 extends _i1.SmartFake + implements _i2.MediaQueryList { + _FakeMediaQueryList_11( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeEntry_12 extends _i1.Fake implements _i2.Entry {} +class _FakeEntry_12 extends _i1.SmartFake implements _i2.Entry { + _FakeEntry_12( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeGeolocation_13 extends _i1.Fake implements _i2.Geolocation {} +class _FakeGeolocation_13 extends _i1.SmartFake implements _i2.Geolocation { + _FakeGeolocation_13( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeMediaStream_14 extends _i1.Fake implements _i2.MediaStream {} +class _FakeMediaStream_14 extends _i1.SmartFake implements _i2.MediaStream { + _FakeMediaStream_14( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeRelatedApplication_15 extends _i1.Fake - implements _i2.RelatedApplication {} +class _FakeRelatedApplication_15 extends _i1.SmartFake + implements _i2.RelatedApplication { + _FakeRelatedApplication_15( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} /// A class which mocks [Window]. /// @@ -60,589 +192,966 @@ class MockWindow extends _i1.Mock implements _i2.Window { } @override - _i3.Future get animationFrame => - (super.noSuchMethod(Invocation.getter(#animationFrame), - returnValue: Future.value(0)) as _i3.Future); - @override - _i2.Document get document => (super.noSuchMethod(Invocation.getter(#document), - returnValue: _FakeDocument_0()) as _i2.Document); - @override - _i2.Location get location => (super.noSuchMethod(Invocation.getter(#location), - returnValue: _FakeLocation_1()) as _i2.Location); - @override - set location(_i2.LocationBase? value) => - super.noSuchMethod(Invocation.setter(#location, value), - returnValueForMissingStub: null); - @override - _i2.Console get console => (super.noSuchMethod(Invocation.getter(#console), - returnValue: _FakeConsole_2()) as _i2.Console); - @override - set defaultStatus(String? value) => - super.noSuchMethod(Invocation.setter(#defaultStatus, value), - returnValueForMissingStub: null); - @override - set defaultstatus(String? value) => - super.noSuchMethod(Invocation.setter(#defaultstatus, value), - returnValueForMissingStub: null); - @override - num get devicePixelRatio => - (super.noSuchMethod(Invocation.getter(#devicePixelRatio), returnValue: 0) - as num); - @override - _i2.History get history => (super.noSuchMethod(Invocation.getter(#history), - returnValue: _FakeHistory_3()) as _i2.History); - @override - _i2.Storage get localStorage => - (super.noSuchMethod(Invocation.getter(#localStorage), - returnValue: _FakeStorage_4()) as _i2.Storage); - @override - set name(String? value) => super.noSuchMethod(Invocation.setter(#name, value), - returnValueForMissingStub: null); - @override - _i2.Navigator get navigator => - (super.noSuchMethod(Invocation.getter(#navigator), - returnValue: _FakeNavigator_5()) as _i2.Navigator); - @override - set opener(_i2.WindowBase? value) => - super.noSuchMethod(Invocation.setter(#opener, value), - returnValueForMissingStub: null); - @override - int get outerHeight => - (super.noSuchMethod(Invocation.getter(#outerHeight), returnValue: 0) - as int); - @override - int get outerWidth => - (super.noSuchMethod(Invocation.getter(#outerWidth), returnValue: 0) - as int); - @override - _i2.Performance get performance => - (super.noSuchMethod(Invocation.getter(#performance), - returnValue: _FakePerformance_6()) as _i2.Performance); - @override - _i2.Storage get sessionStorage => - (super.noSuchMethod(Invocation.getter(#sessionStorage), - returnValue: _FakeStorage_4()) as _i2.Storage); - @override - set status(String? value) => - super.noSuchMethod(Invocation.setter(#status, value), - returnValueForMissingStub: null); - @override - _i3.Stream<_i2.Event> get onContentLoaded => - (super.noSuchMethod(Invocation.getter(#onContentLoaded), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onAbort => - (super.noSuchMethod(Invocation.getter(#onAbort), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onBlur => - (super.noSuchMethod(Invocation.getter(#onBlur), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onCanPlay => - (super.noSuchMethod(Invocation.getter(#onCanPlay), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onCanPlayThrough => - (super.noSuchMethod(Invocation.getter(#onCanPlayThrough), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onChange => - (super.noSuchMethod(Invocation.getter(#onChange), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.MouseEvent> get onClick => - (super.noSuchMethod(Invocation.getter(#onClick), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onContextMenu => - (super.noSuchMethod(Invocation.getter(#onContextMenu), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.Event> get onDoubleClick => - (super.noSuchMethod(Invocation.getter(#onDoubleClick), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.DeviceMotionEvent> get onDeviceMotion => - (super.noSuchMethod(Invocation.getter(#onDeviceMotion), - returnValue: Stream<_i2.DeviceMotionEvent>.empty()) - as _i3.Stream<_i2.DeviceMotionEvent>); + _i3.Future get animationFrame => (super.noSuchMethod( + Invocation.getter(#animationFrame), + returnValue: _i3.Future.value(0), + ) as _i3.Future); + @override + _i2.Document get document => (super.noSuchMethod( + Invocation.getter(#document), + returnValue: _FakeDocument_0( + this, + Invocation.getter(#document), + ), + ) as _i2.Document); + @override + _i2.Location get location => (super.noSuchMethod( + Invocation.getter(#location), + returnValue: _FakeLocation_1( + this, + Invocation.getter(#location), + ), + ) as _i2.Location); + @override + set location(_i2.LocationBase? value) => super.noSuchMethod( + Invocation.setter( + #location, + value, + ), + returnValueForMissingStub: null, + ); + @override + _i2.Console get console => (super.noSuchMethod( + Invocation.getter(#console), + returnValue: _FakeConsole_2( + this, + Invocation.getter(#console), + ), + ) as _i2.Console); + @override + set defaultStatus(String? value) => super.noSuchMethod( + Invocation.setter( + #defaultStatus, + value, + ), + returnValueForMissingStub: null, + ); + @override + set defaultstatus(String? value) => super.noSuchMethod( + Invocation.setter( + #defaultstatus, + value, + ), + returnValueForMissingStub: null, + ); + @override + num get devicePixelRatio => (super.noSuchMethod( + Invocation.getter(#devicePixelRatio), + returnValue: 0, + ) as num); + @override + _i2.History get history => (super.noSuchMethod( + Invocation.getter(#history), + returnValue: _FakeHistory_3( + this, + Invocation.getter(#history), + ), + ) as _i2.History); + @override + _i2.Storage get localStorage => (super.noSuchMethod( + Invocation.getter(#localStorage), + returnValue: _FakeStorage_4( + this, + Invocation.getter(#localStorage), + ), + ) as _i2.Storage); + @override + set name(String? value) => super.noSuchMethod( + Invocation.setter( + #name, + value, + ), + returnValueForMissingStub: null, + ); + @override + _i2.Navigator get navigator => (super.noSuchMethod( + Invocation.getter(#navigator), + returnValue: _FakeNavigator_5( + this, + Invocation.getter(#navigator), + ), + ) as _i2.Navigator); + @override + set opener(_i2.WindowBase? value) => super.noSuchMethod( + Invocation.setter( + #opener, + value, + ), + returnValueForMissingStub: null, + ); + @override + int get outerHeight => (super.noSuchMethod( + Invocation.getter(#outerHeight), + returnValue: 0, + ) as int); + @override + int get outerWidth => (super.noSuchMethod( + Invocation.getter(#outerWidth), + returnValue: 0, + ) as int); + @override + _i2.Performance get performance => (super.noSuchMethod( + Invocation.getter(#performance), + returnValue: _FakePerformance_6( + this, + Invocation.getter(#performance), + ), + ) as _i2.Performance); + @override + _i2.Storage get sessionStorage => (super.noSuchMethod( + Invocation.getter(#sessionStorage), + returnValue: _FakeStorage_4( + this, + Invocation.getter(#sessionStorage), + ), + ) as _i2.Storage); + @override + set status(String? value) => super.noSuchMethod( + Invocation.setter( + #status, + value, + ), + returnValueForMissingStub: null, + ); + @override + _i3.Stream<_i2.Event> get onContentLoaded => (super.noSuchMethod( + Invocation.getter(#onContentLoaded), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onAbort => (super.noSuchMethod( + Invocation.getter(#onAbort), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onBlur => (super.noSuchMethod( + Invocation.getter(#onBlur), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onCanPlay => (super.noSuchMethod( + Invocation.getter(#onCanPlay), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onCanPlayThrough => (super.noSuchMethod( + Invocation.getter(#onCanPlayThrough), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onChange => (super.noSuchMethod( + Invocation.getter(#onChange), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.MouseEvent> get onClick => (super.noSuchMethod( + Invocation.getter(#onClick), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onContextMenu => (super.noSuchMethod( + Invocation.getter(#onContextMenu), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.Event> get onDoubleClick => (super.noSuchMethod( + Invocation.getter(#onDoubleClick), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.DeviceMotionEvent> get onDeviceMotion => (super.noSuchMethod( + Invocation.getter(#onDeviceMotion), + returnValue: _i3.Stream<_i2.DeviceMotionEvent>.empty(), + ) as _i3.Stream<_i2.DeviceMotionEvent>); @override _i3.Stream<_i2.DeviceOrientationEvent> get onDeviceOrientation => - (super.noSuchMethod(Invocation.getter(#onDeviceOrientation), - returnValue: Stream<_i2.DeviceOrientationEvent>.empty()) - as _i3.Stream<_i2.DeviceOrientationEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onDrag => - (super.noSuchMethod(Invocation.getter(#onDrag), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onDragEnd => - (super.noSuchMethod(Invocation.getter(#onDragEnd), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onDragEnter => - (super.noSuchMethod(Invocation.getter(#onDragEnter), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onDragLeave => - (super.noSuchMethod(Invocation.getter(#onDragLeave), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onDragOver => - (super.noSuchMethod(Invocation.getter(#onDragOver), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onDragStart => - (super.noSuchMethod(Invocation.getter(#onDragStart), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onDrop => - (super.noSuchMethod(Invocation.getter(#onDrop), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.Event> get onDurationChange => - (super.noSuchMethod(Invocation.getter(#onDurationChange), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onEmptied => - (super.noSuchMethod(Invocation.getter(#onEmptied), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onEnded => - (super.noSuchMethod(Invocation.getter(#onEnded), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onError => - (super.noSuchMethod(Invocation.getter(#onError), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onFocus => - (super.noSuchMethod(Invocation.getter(#onFocus), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onHashChange => - (super.noSuchMethod(Invocation.getter(#onHashChange), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onInput => - (super.noSuchMethod(Invocation.getter(#onInput), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onInvalid => - (super.noSuchMethod(Invocation.getter(#onInvalid), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.KeyboardEvent> get onKeyDown => - (super.noSuchMethod(Invocation.getter(#onKeyDown), - returnValue: Stream<_i2.KeyboardEvent>.empty()) - as _i3.Stream<_i2.KeyboardEvent>); - @override - _i3.Stream<_i2.KeyboardEvent> get onKeyPress => - (super.noSuchMethod(Invocation.getter(#onKeyPress), - returnValue: Stream<_i2.KeyboardEvent>.empty()) - as _i3.Stream<_i2.KeyboardEvent>); - @override - _i3.Stream<_i2.KeyboardEvent> get onKeyUp => - (super.noSuchMethod(Invocation.getter(#onKeyUp), - returnValue: Stream<_i2.KeyboardEvent>.empty()) - as _i3.Stream<_i2.KeyboardEvent>); - @override - _i3.Stream<_i2.Event> get onLoad => - (super.noSuchMethod(Invocation.getter(#onLoad), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onLoadedData => - (super.noSuchMethod(Invocation.getter(#onLoadedData), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onLoadedMetadata => - (super.noSuchMethod(Invocation.getter(#onLoadedMetadata), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onLoadStart => - (super.noSuchMethod(Invocation.getter(#onLoadStart), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.MessageEvent> get onMessage => - (super.noSuchMethod(Invocation.getter(#onMessage), - returnValue: Stream<_i2.MessageEvent>.empty()) - as _i3.Stream<_i2.MessageEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onMouseDown => - (super.noSuchMethod(Invocation.getter(#onMouseDown), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onMouseEnter => - (super.noSuchMethod(Invocation.getter(#onMouseEnter), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onMouseLeave => - (super.noSuchMethod(Invocation.getter(#onMouseLeave), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onMouseMove => - (super.noSuchMethod(Invocation.getter(#onMouseMove), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onMouseOut => - (super.noSuchMethod(Invocation.getter(#onMouseOut), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onMouseOver => - (super.noSuchMethod(Invocation.getter(#onMouseOver), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.MouseEvent> get onMouseUp => - (super.noSuchMethod(Invocation.getter(#onMouseUp), - returnValue: Stream<_i2.MouseEvent>.empty()) - as _i3.Stream<_i2.MouseEvent>); - @override - _i3.Stream<_i2.WheelEvent> get onMouseWheel => - (super.noSuchMethod(Invocation.getter(#onMouseWheel), - returnValue: Stream<_i2.WheelEvent>.empty()) - as _i3.Stream<_i2.WheelEvent>); - @override - _i3.Stream<_i2.Event> get onOffline => - (super.noSuchMethod(Invocation.getter(#onOffline), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onOnline => - (super.noSuchMethod(Invocation.getter(#onOnline), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onPageHide => - (super.noSuchMethod(Invocation.getter(#onPageHide), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onPageShow => - (super.noSuchMethod(Invocation.getter(#onPageShow), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onPause => - (super.noSuchMethod(Invocation.getter(#onPause), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onPlay => - (super.noSuchMethod(Invocation.getter(#onPlay), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onPlaying => - (super.noSuchMethod(Invocation.getter(#onPlaying), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.PopStateEvent> get onPopState => - (super.noSuchMethod(Invocation.getter(#onPopState), - returnValue: Stream<_i2.PopStateEvent>.empty()) - as _i3.Stream<_i2.PopStateEvent>); - @override - _i3.Stream<_i2.Event> get onProgress => - (super.noSuchMethod(Invocation.getter(#onProgress), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onRateChange => - (super.noSuchMethod(Invocation.getter(#onRateChange), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onReset => - (super.noSuchMethod(Invocation.getter(#onReset), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onResize => - (super.noSuchMethod(Invocation.getter(#onResize), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onScroll => - (super.noSuchMethod(Invocation.getter(#onScroll), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onSearch => - (super.noSuchMethod(Invocation.getter(#onSearch), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onSeeked => - (super.noSuchMethod(Invocation.getter(#onSeeked), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onSeeking => - (super.noSuchMethod(Invocation.getter(#onSeeking), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onSelect => - (super.noSuchMethod(Invocation.getter(#onSelect), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onStalled => - (super.noSuchMethod(Invocation.getter(#onStalled), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.StorageEvent> get onStorage => - (super.noSuchMethod(Invocation.getter(#onStorage), - returnValue: Stream<_i2.StorageEvent>.empty()) - as _i3.Stream<_i2.StorageEvent>); - @override - _i3.Stream<_i2.Event> get onSubmit => - (super.noSuchMethod(Invocation.getter(#onSubmit), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onSuspend => - (super.noSuchMethod(Invocation.getter(#onSuspend), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onTimeUpdate => - (super.noSuchMethod(Invocation.getter(#onTimeUpdate), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.TouchEvent> get onTouchCancel => - (super.noSuchMethod(Invocation.getter(#onTouchCancel), - returnValue: Stream<_i2.TouchEvent>.empty()) - as _i3.Stream<_i2.TouchEvent>); - @override - _i3.Stream<_i2.TouchEvent> get onTouchEnd => - (super.noSuchMethod(Invocation.getter(#onTouchEnd), - returnValue: Stream<_i2.TouchEvent>.empty()) - as _i3.Stream<_i2.TouchEvent>); - @override - _i3.Stream<_i2.TouchEvent> get onTouchMove => - (super.noSuchMethod(Invocation.getter(#onTouchMove), - returnValue: Stream<_i2.TouchEvent>.empty()) - as _i3.Stream<_i2.TouchEvent>); - @override - _i3.Stream<_i2.TouchEvent> get onTouchStart => - (super.noSuchMethod(Invocation.getter(#onTouchStart), - returnValue: Stream<_i2.TouchEvent>.empty()) - as _i3.Stream<_i2.TouchEvent>); - @override - _i3.Stream<_i2.TransitionEvent> get onTransitionEnd => - (super.noSuchMethod(Invocation.getter(#onTransitionEnd), - returnValue: Stream<_i2.TransitionEvent>.empty()) - as _i3.Stream<_i2.TransitionEvent>); - @override - _i3.Stream<_i2.Event> get onUnload => - (super.noSuchMethod(Invocation.getter(#onUnload), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onVolumeChange => - (super.noSuchMethod(Invocation.getter(#onVolumeChange), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.Event> get onWaiting => - (super.noSuchMethod(Invocation.getter(#onWaiting), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.AnimationEvent> get onAnimationEnd => - (super.noSuchMethod(Invocation.getter(#onAnimationEnd), - returnValue: Stream<_i2.AnimationEvent>.empty()) - as _i3.Stream<_i2.AnimationEvent>); + (super.noSuchMethod( + Invocation.getter(#onDeviceOrientation), + returnValue: _i3.Stream<_i2.DeviceOrientationEvent>.empty(), + ) as _i3.Stream<_i2.DeviceOrientationEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onDrag => (super.noSuchMethod( + Invocation.getter(#onDrag), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onDragEnd => (super.noSuchMethod( + Invocation.getter(#onDragEnd), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onDragEnter => (super.noSuchMethod( + Invocation.getter(#onDragEnter), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onDragLeave => (super.noSuchMethod( + Invocation.getter(#onDragLeave), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onDragOver => (super.noSuchMethod( + Invocation.getter(#onDragOver), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onDragStart => (super.noSuchMethod( + Invocation.getter(#onDragStart), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onDrop => (super.noSuchMethod( + Invocation.getter(#onDrop), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.Event> get onDurationChange => (super.noSuchMethod( + Invocation.getter(#onDurationChange), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onEmptied => (super.noSuchMethod( + Invocation.getter(#onEmptied), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onEnded => (super.noSuchMethod( + Invocation.getter(#onEnded), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onError => (super.noSuchMethod( + Invocation.getter(#onError), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onFocus => (super.noSuchMethod( + Invocation.getter(#onFocus), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onHashChange => (super.noSuchMethod( + Invocation.getter(#onHashChange), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onInput => (super.noSuchMethod( + Invocation.getter(#onInput), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onInvalid => (super.noSuchMethod( + Invocation.getter(#onInvalid), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.KeyboardEvent> get onKeyDown => (super.noSuchMethod( + Invocation.getter(#onKeyDown), + returnValue: _i3.Stream<_i2.KeyboardEvent>.empty(), + ) as _i3.Stream<_i2.KeyboardEvent>); + @override + _i3.Stream<_i2.KeyboardEvent> get onKeyPress => (super.noSuchMethod( + Invocation.getter(#onKeyPress), + returnValue: _i3.Stream<_i2.KeyboardEvent>.empty(), + ) as _i3.Stream<_i2.KeyboardEvent>); + @override + _i3.Stream<_i2.KeyboardEvent> get onKeyUp => (super.noSuchMethod( + Invocation.getter(#onKeyUp), + returnValue: _i3.Stream<_i2.KeyboardEvent>.empty(), + ) as _i3.Stream<_i2.KeyboardEvent>); + @override + _i3.Stream<_i2.Event> get onLoad => (super.noSuchMethod( + Invocation.getter(#onLoad), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onLoadedData => (super.noSuchMethod( + Invocation.getter(#onLoadedData), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onLoadedMetadata => (super.noSuchMethod( + Invocation.getter(#onLoadedMetadata), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onLoadStart => (super.noSuchMethod( + Invocation.getter(#onLoadStart), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.MessageEvent> get onMessage => (super.noSuchMethod( + Invocation.getter(#onMessage), + returnValue: _i3.Stream<_i2.MessageEvent>.empty(), + ) as _i3.Stream<_i2.MessageEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onMouseDown => (super.noSuchMethod( + Invocation.getter(#onMouseDown), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onMouseEnter => (super.noSuchMethod( + Invocation.getter(#onMouseEnter), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onMouseLeave => (super.noSuchMethod( + Invocation.getter(#onMouseLeave), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onMouseMove => (super.noSuchMethod( + Invocation.getter(#onMouseMove), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onMouseOut => (super.noSuchMethod( + Invocation.getter(#onMouseOut), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onMouseOver => (super.noSuchMethod( + Invocation.getter(#onMouseOver), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.MouseEvent> get onMouseUp => (super.noSuchMethod( + Invocation.getter(#onMouseUp), + returnValue: _i3.Stream<_i2.MouseEvent>.empty(), + ) as _i3.Stream<_i2.MouseEvent>); + @override + _i3.Stream<_i2.WheelEvent> get onMouseWheel => (super.noSuchMethod( + Invocation.getter(#onMouseWheel), + returnValue: _i3.Stream<_i2.WheelEvent>.empty(), + ) as _i3.Stream<_i2.WheelEvent>); + @override + _i3.Stream<_i2.Event> get onOffline => (super.noSuchMethod( + Invocation.getter(#onOffline), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onOnline => (super.noSuchMethod( + Invocation.getter(#onOnline), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onPageHide => (super.noSuchMethod( + Invocation.getter(#onPageHide), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onPageShow => (super.noSuchMethod( + Invocation.getter(#onPageShow), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onPause => (super.noSuchMethod( + Invocation.getter(#onPause), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onPlay => (super.noSuchMethod( + Invocation.getter(#onPlay), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onPlaying => (super.noSuchMethod( + Invocation.getter(#onPlaying), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.PopStateEvent> get onPopState => (super.noSuchMethod( + Invocation.getter(#onPopState), + returnValue: _i3.Stream<_i2.PopStateEvent>.empty(), + ) as _i3.Stream<_i2.PopStateEvent>); + @override + _i3.Stream<_i2.Event> get onProgress => (super.noSuchMethod( + Invocation.getter(#onProgress), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onRateChange => (super.noSuchMethod( + Invocation.getter(#onRateChange), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onReset => (super.noSuchMethod( + Invocation.getter(#onReset), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onResize => (super.noSuchMethod( + Invocation.getter(#onResize), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onScroll => (super.noSuchMethod( + Invocation.getter(#onScroll), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onSearch => (super.noSuchMethod( + Invocation.getter(#onSearch), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onSeeked => (super.noSuchMethod( + Invocation.getter(#onSeeked), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onSeeking => (super.noSuchMethod( + Invocation.getter(#onSeeking), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onSelect => (super.noSuchMethod( + Invocation.getter(#onSelect), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onStalled => (super.noSuchMethod( + Invocation.getter(#onStalled), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.StorageEvent> get onStorage => (super.noSuchMethod( + Invocation.getter(#onStorage), + returnValue: _i3.Stream<_i2.StorageEvent>.empty(), + ) as _i3.Stream<_i2.StorageEvent>); + @override + _i3.Stream<_i2.Event> get onSubmit => (super.noSuchMethod( + Invocation.getter(#onSubmit), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onSuspend => (super.noSuchMethod( + Invocation.getter(#onSuspend), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onTimeUpdate => (super.noSuchMethod( + Invocation.getter(#onTimeUpdate), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.TouchEvent> get onTouchCancel => (super.noSuchMethod( + Invocation.getter(#onTouchCancel), + returnValue: _i3.Stream<_i2.TouchEvent>.empty(), + ) as _i3.Stream<_i2.TouchEvent>); + @override + _i3.Stream<_i2.TouchEvent> get onTouchEnd => (super.noSuchMethod( + Invocation.getter(#onTouchEnd), + returnValue: _i3.Stream<_i2.TouchEvent>.empty(), + ) as _i3.Stream<_i2.TouchEvent>); + @override + _i3.Stream<_i2.TouchEvent> get onTouchMove => (super.noSuchMethod( + Invocation.getter(#onTouchMove), + returnValue: _i3.Stream<_i2.TouchEvent>.empty(), + ) as _i3.Stream<_i2.TouchEvent>); + @override + _i3.Stream<_i2.TouchEvent> get onTouchStart => (super.noSuchMethod( + Invocation.getter(#onTouchStart), + returnValue: _i3.Stream<_i2.TouchEvent>.empty(), + ) as _i3.Stream<_i2.TouchEvent>); + @override + _i3.Stream<_i2.TransitionEvent> get onTransitionEnd => (super.noSuchMethod( + Invocation.getter(#onTransitionEnd), + returnValue: _i3.Stream<_i2.TransitionEvent>.empty(), + ) as _i3.Stream<_i2.TransitionEvent>); + @override + _i3.Stream<_i2.Event> get onUnload => (super.noSuchMethod( + Invocation.getter(#onUnload), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onVolumeChange => (super.noSuchMethod( + Invocation.getter(#onVolumeChange), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.Event> get onWaiting => (super.noSuchMethod( + Invocation.getter(#onWaiting), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.AnimationEvent> get onAnimationEnd => (super.noSuchMethod( + Invocation.getter(#onAnimationEnd), + returnValue: _i3.Stream<_i2.AnimationEvent>.empty(), + ) as _i3.Stream<_i2.AnimationEvent>); @override _i3.Stream<_i2.AnimationEvent> get onAnimationIteration => - (super.noSuchMethod(Invocation.getter(#onAnimationIteration), - returnValue: Stream<_i2.AnimationEvent>.empty()) - as _i3.Stream<_i2.AnimationEvent>); - @override - _i3.Stream<_i2.AnimationEvent> get onAnimationStart => - (super.noSuchMethod(Invocation.getter(#onAnimationStart), - returnValue: Stream<_i2.AnimationEvent>.empty()) - as _i3.Stream<_i2.AnimationEvent>); - @override - _i3.Stream<_i2.Event> get onBeforeUnload => - (super.noSuchMethod(Invocation.getter(#onBeforeUnload), - returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); - @override - _i3.Stream<_i2.WheelEvent> get onWheel => - (super.noSuchMethod(Invocation.getter(#onWheel), - returnValue: Stream<_i2.WheelEvent>.empty()) - as _i3.Stream<_i2.WheelEvent>); - @override - int get pageXOffset => - (super.noSuchMethod(Invocation.getter(#pageXOffset), returnValue: 0) - as int); - @override - int get pageYOffset => - (super.noSuchMethod(Invocation.getter(#pageYOffset), returnValue: 0) - as int); - @override - int get scrollX => - (super.noSuchMethod(Invocation.getter(#scrollX), returnValue: 0) as int); - @override - int get scrollY => - (super.noSuchMethod(Invocation.getter(#scrollY), returnValue: 0) as int); - @override - _i2.Events get on => - (super.noSuchMethod(Invocation.getter(#on), returnValue: _FakeEvents_7()) - as _i2.Events); - @override - _i2.WindowBase open(String? url, String? name, [String? options]) => - (super.noSuchMethod(Invocation.method(#open, [url, name, options]), - returnValue: _FakeWindowBase_8()) as _i2.WindowBase); + (super.noSuchMethod( + Invocation.getter(#onAnimationIteration), + returnValue: _i3.Stream<_i2.AnimationEvent>.empty(), + ) as _i3.Stream<_i2.AnimationEvent>); + @override + _i3.Stream<_i2.AnimationEvent> get onAnimationStart => (super.noSuchMethod( + Invocation.getter(#onAnimationStart), + returnValue: _i3.Stream<_i2.AnimationEvent>.empty(), + ) as _i3.Stream<_i2.AnimationEvent>); + @override + _i3.Stream<_i2.Event> get onBeforeUnload => (super.noSuchMethod( + Invocation.getter(#onBeforeUnload), + returnValue: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.WheelEvent> get onWheel => (super.noSuchMethod( + Invocation.getter(#onWheel), + returnValue: _i3.Stream<_i2.WheelEvent>.empty(), + ) as _i3.Stream<_i2.WheelEvent>); + @override + int get pageXOffset => (super.noSuchMethod( + Invocation.getter(#pageXOffset), + returnValue: 0, + ) as int); + @override + int get pageYOffset => (super.noSuchMethod( + Invocation.getter(#pageYOffset), + returnValue: 0, + ) as int); + @override + int get scrollX => (super.noSuchMethod( + Invocation.getter(#scrollX), + returnValue: 0, + ) as int); + @override + int get scrollY => (super.noSuchMethod( + Invocation.getter(#scrollY), + returnValue: 0, + ) as int); + @override + _i2.Events get on => (super.noSuchMethod( + Invocation.getter(#on), + returnValue: _FakeEvents_7( + this, + Invocation.getter(#on), + ), + ) as _i2.Events); + @override + _i2.WindowBase open( + String? url, + String? name, [ + String? options, + ]) => + (super.noSuchMethod( + Invocation.method( + #open, + [ + url, + name, + options, + ], + ), + returnValue: _FakeWindowBase_8( + this, + Invocation.method( + #open, + [ + url, + name, + options, + ], + ), + ), + ) as _i2.WindowBase); @override int requestAnimationFrame(_i2.FrameRequestCallback? callback) => - (super.noSuchMethod(Invocation.method(#requestAnimationFrame, [callback]), - returnValue: 0) as int); - @override - void cancelAnimationFrame(int? id) => - super.noSuchMethod(Invocation.method(#cancelAnimationFrame, [id]), - returnValueForMissingStub: null); - @override - _i3.Future<_i2.FileSystem> requestFileSystem(int? size, - {bool? persistent = false}) => (super.noSuchMethod( - Invocation.method( - #requestFileSystem, [size], {#persistent: persistent}), - returnValue: Future<_i2.FileSystem>.value(_FakeFileSystem_9())) - as _i3.Future<_i2.FileSystem>); - @override - void alert([String? message]) => - super.noSuchMethod(Invocation.method(#alert, [message]), - returnValueForMissingStub: null); - @override - void cancelIdleCallback(int? handle) => - super.noSuchMethod(Invocation.method(#cancelIdleCallback, [handle]), - returnValueForMissingStub: null); - @override - void close() => super.noSuchMethod(Invocation.method(#close, []), - returnValueForMissingStub: null); - @override - bool confirm([String? message]) => - (super.noSuchMethod(Invocation.method(#confirm, [message]), - returnValue: false) as bool); - @override - _i3.Future fetch(dynamic input, [Map? init]) => - (super.noSuchMethod(Invocation.method(#fetch, [input, init]), - returnValue: Future.value()) as _i3.Future); - @override - bool find(String? string, bool? caseSensitive, bool? backwards, bool? wrap, - bool? wholeWord, bool? searchInFrames, bool? showDialog) => + Invocation.method( + #requestAnimationFrame, + [callback], + ), + returnValue: 0, + ) as int); + @override + void cancelAnimationFrame(int? id) => super.noSuchMethod( + Invocation.method( + #cancelAnimationFrame, + [id], + ), + returnValueForMissingStub: null, + ); + @override + _i3.Future<_i2.FileSystem> requestFileSystem( + int? size, { + bool? persistent = false, + }) => + (super.noSuchMethod( + Invocation.method( + #requestFileSystem, + [size], + {#persistent: persistent}, + ), + returnValue: _i3.Future<_i2.FileSystem>.value(_FakeFileSystem_9( + this, + Invocation.method( + #requestFileSystem, + [size], + {#persistent: persistent}, + ), + )), + ) as _i3.Future<_i2.FileSystem>); + @override + void alert([String? message]) => super.noSuchMethod( + Invocation.method( + #alert, + [message], + ), + returnValueForMissingStub: null, + ); + @override + void cancelIdleCallback(int? handle) => super.noSuchMethod( + Invocation.method( + #cancelIdleCallback, + [handle], + ), + returnValueForMissingStub: null, + ); + @override + void close() => super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValueForMissingStub: null, + ); + @override + bool confirm([String? message]) => (super.noSuchMethod( + Invocation.method( + #confirm, + [message], + ), + returnValue: false, + ) as bool); + @override + _i3.Future fetch( + dynamic input, [ + Map? init, + ]) => + (super.noSuchMethod( + Invocation.method( + #fetch, + [ + input, + init, + ], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + @override + bool find( + String? string, + bool? caseSensitive, + bool? backwards, + bool? wrap, + bool? wholeWord, + bool? searchInFrames, + bool? showDialog, + ) => (super.noSuchMethod( - Invocation.method(#find, [ + Invocation.method( + #find, + [ string, caseSensitive, backwards, wrap, wholeWord, searchInFrames, - showDialog - ]), - returnValue: false) as bool); + showDialog, + ], + ), + returnValue: false, + ) as bool); @override _i2.StylePropertyMapReadonly getComputedStyleMap( - _i2.Element? element, String? pseudoElement) => + _i2.Element? element, + String? pseudoElement, + ) => (super.noSuchMethod( - Invocation.method(#getComputedStyleMap, [element, pseudoElement]), - returnValue: _FakeStylePropertyMapReadonly_10()) - as _i2.StylePropertyMapReadonly); + Invocation.method( + #getComputedStyleMap, + [ + element, + pseudoElement, + ], + ), + returnValue: _FakeStylePropertyMapReadonly_10( + this, + Invocation.method( + #getComputedStyleMap, + [ + element, + pseudoElement, + ], + ), + ), + ) as _i2.StylePropertyMapReadonly); @override List<_i2.CssRule> getMatchedCssRules( - _i2.Element? element, String? pseudoElement) => + _i2.Element? element, + String? pseudoElement, + ) => (super.noSuchMethod( - Invocation.method(#getMatchedCssRules, [element, pseudoElement]), - returnValue: <_i2.CssRule>[]) as List<_i2.CssRule>); - @override - _i2.MediaQueryList matchMedia(String? query) => - (super.noSuchMethod(Invocation.method(#matchMedia, [query]), - returnValue: _FakeMediaQueryList_11()) as _i2.MediaQueryList); - @override - void moveBy(int? x, int? y) => - super.noSuchMethod(Invocation.method(#moveBy, [x, y]), - returnValueForMissingStub: null); - @override - void postMessage(dynamic message, String? targetOrigin, - [List? transfer]) => + Invocation.method( + #getMatchedCssRules, + [ + element, + pseudoElement, + ], + ), + returnValue: <_i2.CssRule>[], + ) as List<_i2.CssRule>); + @override + _i2.MediaQueryList matchMedia(String? query) => (super.noSuchMethod( + Invocation.method( + #matchMedia, + [query], + ), + returnValue: _FakeMediaQueryList_11( + this, + Invocation.method( + #matchMedia, + [query], + ), + ), + ) as _i2.MediaQueryList); + @override + void moveBy( + int? x, + int? y, + ) => super.noSuchMethod( - Invocation.method(#postMessage, [message, targetOrigin, transfer]), - returnValueForMissingStub: null); - @override - void print() => super.noSuchMethod(Invocation.method(#print, []), - returnValueForMissingStub: null); - @override - int requestIdleCallback(_i2.IdleRequestCallback? callback, - [Map? options]) => + Invocation.method( + #moveBy, + [ + x, + y, + ], + ), + returnValueForMissingStub: null, + ); + @override + void postMessage( + dynamic message, + String? targetOrigin, [ + List? transfer, + ]) => + super.noSuchMethod( + Invocation.method( + #postMessage, + [ + message, + targetOrigin, + transfer, + ], + ), + returnValueForMissingStub: null, + ); + @override + void print() => super.noSuchMethod( + Invocation.method( + #print, + [], + ), + returnValueForMissingStub: null, + ); + @override + int requestIdleCallback( + _i2.IdleRequestCallback? callback, [ + Map? options, + ]) => (super.noSuchMethod( - Invocation.method(#requestIdleCallback, [callback, options]), - returnValue: 0) as int); - @override - void resizeBy(int? x, int? y) => - super.noSuchMethod(Invocation.method(#resizeBy, [x, y]), - returnValueForMissingStub: null); - @override - void resizeTo(int? x, int? y) => - super.noSuchMethod(Invocation.method(#resizeTo, [x, y]), - returnValueForMissingStub: null); - @override - void scroll( - [dynamic options_OR_x, - dynamic y, - Map? scrollOptions]) => + Invocation.method( + #requestIdleCallback, + [ + callback, + options, + ], + ), + returnValue: 0, + ) as int); + @override + void resizeBy( + int? x, + int? y, + ) => super.noSuchMethod( - Invocation.method(#scroll, [options_OR_x, y, scrollOptions]), - returnValueForMissingStub: null); - @override - void scrollBy( - [dynamic options_OR_x, - dynamic y, - Map? scrollOptions]) => + Invocation.method( + #resizeBy, + [ + x, + y, + ], + ), + returnValueForMissingStub: null, + ); + @override + void resizeTo( + int? x, + int? y, + ) => super.noSuchMethod( - Invocation.method(#scrollBy, [options_OR_x, y, scrollOptions]), - returnValueForMissingStub: null); - @override - void scrollTo( - [dynamic options_OR_x, - dynamic y, - Map? scrollOptions]) => + Invocation.method( + #resizeTo, + [ + x, + y, + ], + ), + returnValueForMissingStub: null, + ); + @override + void scroll([ + dynamic options_OR_x, + dynamic y, + Map? scrollOptions, + ]) => super.noSuchMethod( - Invocation.method(#scrollTo, [options_OR_x, y, scrollOptions]), - returnValueForMissingStub: null); - @override - void stop() => super.noSuchMethod(Invocation.method(#stop, []), - returnValueForMissingStub: null); + Invocation.method( + #scroll, + [ + options_OR_x, + y, + scrollOptions, + ], + ), + returnValueForMissingStub: null, + ); + @override + void scrollBy([ + dynamic options_OR_x, + dynamic y, + Map? scrollOptions, + ]) => + super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + options_OR_x, + y, + scrollOptions, + ], + ), + returnValueForMissingStub: null, + ); + @override + void scrollTo([ + dynamic options_OR_x, + dynamic y, + Map? scrollOptions, + ]) => + super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + options_OR_x, + y, + scrollOptions, + ], + ), + returnValueForMissingStub: null, + ); + @override + void stop() => super.noSuchMethod( + Invocation.method( + #stop, + [], + ), + returnValueForMissingStub: null, + ); @override _i3.Future<_i2.Entry> resolveLocalFileSystemUrl(String? url) => - (super.noSuchMethod(Invocation.method(#resolveLocalFileSystemUrl, [url]), - returnValue: Future<_i2.Entry>.value(_FakeEntry_12())) - as _i3.Future<_i2.Entry>); - @override - String atob(String? atob) => - (super.noSuchMethod(Invocation.method(#atob, [atob]), returnValue: '') - as String); - @override - String btoa(String? btoa) => - (super.noSuchMethod(Invocation.method(#btoa, [btoa]), returnValue: '') - as String); - @override - void moveTo(_i4.Point? p) => - super.noSuchMethod(Invocation.method(#moveTo, [p]), - returnValueForMissingStub: null); - @override - void addEventListener(String? type, _i2.EventListener? listener, - [bool? useCapture]) => + (super.noSuchMethod( + Invocation.method( + #resolveLocalFileSystemUrl, + [url], + ), + returnValue: _i3.Future<_i2.Entry>.value(_FakeEntry_12( + this, + Invocation.method( + #resolveLocalFileSystemUrl, + [url], + ), + )), + ) as _i3.Future<_i2.Entry>); + @override + String atob(String? atob) => (super.noSuchMethod( + Invocation.method( + #atob, + [atob], + ), + returnValue: '', + ) as String); + @override + String btoa(String? btoa) => (super.noSuchMethod( + Invocation.method( + #btoa, + [btoa], + ), + returnValue: '', + ) as String); + @override + void moveTo(_i4.Point? p) => super.noSuchMethod( + Invocation.method( + #moveTo, + [p], + ), + returnValueForMissingStub: null, + ); + @override + void addEventListener( + String? type, + _i2.EventListener? listener, [ + bool? useCapture, + ]) => super.noSuchMethod( - Invocation.method(#addEventListener, [type, listener, useCapture]), - returnValueForMissingStub: null); - @override - void removeEventListener(String? type, _i2.EventListener? listener, - [bool? useCapture]) => + Invocation.method( + #addEventListener, + [ + type, + listener, + useCapture, + ], + ), + returnValueForMissingStub: null, + ); + @override + void removeEventListener( + String? type, + _i2.EventListener? listener, [ + bool? useCapture, + ]) => super.noSuchMethod( - Invocation.method(#removeEventListener, [type, listener, useCapture]), - returnValueForMissingStub: null); - @override - bool dispatchEvent(_i2.Event? event) => - (super.noSuchMethod(Invocation.method(#dispatchEvent, [event]), - returnValue: false) as bool); + Invocation.method( + #removeEventListener, + [ + type, + listener, + useCapture, + ], + ), + returnValueForMissingStub: null, + ); + @override + bool dispatchEvent(_i2.Event? event) => (super.noSuchMethod( + Invocation.method( + #dispatchEvent, + [event], + ), + returnValue: false, + ) as bool); } /// A class which mocks [Navigator]. @@ -654,97 +1163,199 @@ class MockNavigator extends _i1.Mock implements _i2.Navigator { } @override - String get language => - (super.noSuchMethod(Invocation.getter(#language), returnValue: '') - as String); - @override - _i2.Geolocation get geolocation => - (super.noSuchMethod(Invocation.getter(#geolocation), - returnValue: _FakeGeolocation_13()) as _i2.Geolocation); - @override - String get vendor => - (super.noSuchMethod(Invocation.getter(#vendor), returnValue: '') - as String); - @override - String get vendorSub => - (super.noSuchMethod(Invocation.getter(#vendorSub), returnValue: '') - as String); - @override - String get appCodeName => - (super.noSuchMethod(Invocation.getter(#appCodeName), returnValue: '') - as String); - @override - String get appName => - (super.noSuchMethod(Invocation.getter(#appName), returnValue: '') - as String); - @override - String get appVersion => - (super.noSuchMethod(Invocation.getter(#appVersion), returnValue: '') - as String); - @override - String get product => - (super.noSuchMethod(Invocation.getter(#product), returnValue: '') - as String); - @override - String get userAgent => - (super.noSuchMethod(Invocation.getter(#userAgent), returnValue: '') - as String); - @override - List<_i2.Gamepad?> getGamepads() => - (super.noSuchMethod(Invocation.method(#getGamepads, []), - returnValue: <_i2.Gamepad?>[]) as List<_i2.Gamepad?>); - @override - _i3.Future<_i2.MediaStream> getUserMedia( - {dynamic audio = false, dynamic video = false}) => + String get language => (super.noSuchMethod( + Invocation.getter(#language), + returnValue: '', + ) as String); + @override + _i2.Geolocation get geolocation => (super.noSuchMethod( + Invocation.getter(#geolocation), + returnValue: _FakeGeolocation_13( + this, + Invocation.getter(#geolocation), + ), + ) as _i2.Geolocation); + @override + String get vendor => (super.noSuchMethod( + Invocation.getter(#vendor), + returnValue: '', + ) as String); + @override + String get vendorSub => (super.noSuchMethod( + Invocation.getter(#vendorSub), + returnValue: '', + ) as String); + @override + String get appCodeName => (super.noSuchMethod( + Invocation.getter(#appCodeName), + returnValue: '', + ) as String); + @override + String get appName => (super.noSuchMethod( + Invocation.getter(#appName), + returnValue: '', + ) as String); + @override + String get appVersion => (super.noSuchMethod( + Invocation.getter(#appVersion), + returnValue: '', + ) as String); + @override + String get product => (super.noSuchMethod( + Invocation.getter(#product), + returnValue: '', + ) as String); + @override + String get userAgent => (super.noSuchMethod( + Invocation.getter(#userAgent), + returnValue: '', + ) as String); + @override + List<_i2.Gamepad?> getGamepads() => (super.noSuchMethod( + Invocation.method( + #getGamepads, + [], + ), + returnValue: <_i2.Gamepad?>[], + ) as List<_i2.Gamepad?>); + @override + _i3.Future<_i2.MediaStream> getUserMedia({ + dynamic audio = false, + dynamic video = false, + }) => (super.noSuchMethod( - Invocation.method(#getUserMedia, [], {#audio: audio, #video: video}), - returnValue: - Future<_i2.MediaStream>.value(_FakeMediaStream_14())) as _i3 - .Future<_i2.MediaStream>); - @override - void cancelKeyboardLock() => - super.noSuchMethod(Invocation.method(#cancelKeyboardLock, []), - returnValueForMissingStub: null); - @override - _i3.Future getBattery() => - (super.noSuchMethod(Invocation.method(#getBattery, []), - returnValue: Future.value()) as _i3.Future); + Invocation.method( + #getUserMedia, + [], + { + #audio: audio, + #video: video, + }, + ), + returnValue: _i3.Future<_i2.MediaStream>.value(_FakeMediaStream_14( + this, + Invocation.method( + #getUserMedia, + [], + { + #audio: audio, + #video: video, + }, + ), + )), + ) as _i3.Future<_i2.MediaStream>); + @override + void cancelKeyboardLock() => super.noSuchMethod( + Invocation.method( + #cancelKeyboardLock, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i3.Future getBattery() => (super.noSuchMethod( + Invocation.method( + #getBattery, + [], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); @override _i3.Future<_i2.RelatedApplication> getInstalledRelatedApps() => - (super.noSuchMethod(Invocation.method(#getInstalledRelatedApps, []), - returnValue: Future<_i2.RelatedApplication>.value( - _FakeRelatedApplication_15())) - as _i3.Future<_i2.RelatedApplication>); - @override - _i3.Future getVRDisplays() => - (super.noSuchMethod(Invocation.method(#getVRDisplays, []), - returnValue: Future.value()) as _i3.Future); - @override - void registerProtocolHandler(String? scheme, String? url, String? title) => + (super.noSuchMethod( + Invocation.method( + #getInstalledRelatedApps, + [], + ), + returnValue: + _i3.Future<_i2.RelatedApplication>.value(_FakeRelatedApplication_15( + this, + Invocation.method( + #getInstalledRelatedApps, + [], + ), + )), + ) as _i3.Future<_i2.RelatedApplication>); + @override + _i3.Future getVRDisplays() => (super.noSuchMethod( + Invocation.method( + #getVRDisplays, + [], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + @override + void registerProtocolHandler( + String? scheme, + String? url, + String? title, + ) => super.noSuchMethod( - Invocation.method(#registerProtocolHandler, [scheme, url, title]), - returnValueForMissingStub: null); + Invocation.method( + #registerProtocolHandler, + [ + scheme, + url, + title, + ], + ), + returnValueForMissingStub: null, + ); @override _i3.Future requestKeyboardLock([List? keyCodes]) => - (super.noSuchMethod(Invocation.method(#requestKeyboardLock, [keyCodes]), - returnValue: Future.value()) as _i3.Future); + (super.noSuchMethod( + Invocation.method( + #requestKeyboardLock, + [keyCodes], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); @override _i3.Future requestMidiAccess([Map? options]) => - (super.noSuchMethod(Invocation.method(#requestMidiAccess, [options]), - returnValue: Future.value()) as _i3.Future); - @override - _i3.Future requestMediaKeySystemAccess(String? keySystem, - List>? supportedConfigurations) => (super.noSuchMethod( - Invocation.method(#requestMediaKeySystemAccess, - [keySystem, supportedConfigurations]), - returnValue: Future.value()) as _i3.Future); - @override - bool sendBeacon(String? url, Object? data) => - (super.noSuchMethod(Invocation.method(#sendBeacon, [url, data]), - returnValue: false) as bool); + Invocation.method( + #requestMidiAccess, + [options], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future requestMediaKeySystemAccess( + String? keySystem, + List>? supportedConfigurations, + ) => + (super.noSuchMethod( + Invocation.method( + #requestMediaKeySystemAccess, + [ + keySystem, + supportedConfigurations, + ], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + @override + bool sendBeacon( + String? url, + Object? data, + ) => + (super.noSuchMethod( + Invocation.method( + #sendBeacon, + [ + url, + data, + ], + ), + returnValue: false, + ) as bool); @override _i3.Future share([Map? data]) => - (super.noSuchMethod(Invocation.method(#share, [data]), - returnValue: Future.value()) as _i3.Future); + (super.noSuchMethod( + Invocation.method( + #share, + [data], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); } diff --git a/packages/url_launcher/url_launcher_web/example/lib/main.dart b/packages/url_launcher/url_launcher_web/example/lib/main.dart index 341913a18490..87422953de6a 100644 --- a/packages/url_launcher/url_launcher_web/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_web/example/lib/main.dart @@ -5,13 +5,16 @@ import 'package:flutter/material.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } /// App for testing class MyApp extends StatefulWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { diff --git a/packages/url_launcher/url_launcher_web/example/pubspec.yaml b/packages/url_launcher/url_launcher_web/example/pubspec.yaml index bface463cfe2..f972b2857ecf 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.2.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -15,8 +15,11 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter + flutter_web_plugins: + sdk: flutter integration_test: sdk: flutter - mockito: ^5.0.0 + mockito: ^5.3.2 + url_launcher_platform_interface: ^2.0.3 url_launcher_web: path: ../ 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 4498e74ea9ce..112d07ea7571 100644 --- a/packages/url_launcher/url_launcher_web/lib/src/link.dart +++ b/packages/url_launcher/url_launcher_web/lib/src/link.dart @@ -31,7 +31,7 @@ HtmlViewFactory get linkViewFactory => LinkViewController._viewFactory; /// It uses a platform view to render an anchor element in the DOM. class WebLinkDelegate extends StatefulWidget { /// Creates a delegate for the given [link]. - const WebLinkDelegate(this.link); + const WebLinkDelegate(this.link, {Key? key}) : super(key: key); /// Information about the link built by the app. final LinkInfo link; @@ -76,7 +76,7 @@ class WebLinkDelegateState extends State { child: PlatformViewLink( viewType: linkViewType, onCreatePlatformView: (PlatformViewCreationParams params) { - _controller = LinkViewController.fromParams(params, context); + _controller = LinkViewController.fromParams(params); return _controller ..setUri(widget.link.uri) ..setTarget(widget.link.target); @@ -100,7 +100,7 @@ class WebLinkDelegateState extends State { /// Controls link views. class LinkViewController extends PlatformViewController { /// Creates a [LinkViewController] instance with the unique [viewId]. - LinkViewController(this.viewId, this.context) { + LinkViewController(this.viewId) { if (_instances.isEmpty) { // This is the first controller being created, attach the global click // listener. @@ -113,12 +113,17 @@ class LinkViewController extends PlatformViewController { /// platform view [params]. factory LinkViewController.fromParams( PlatformViewCreationParams params, - BuildContext context, ) { final int viewId = params.id; - final LinkViewController controller = LinkViewController(viewId, context); + final LinkViewController controller = LinkViewController(viewId); controller._initialize().then((_) { - params.onPlatformViewCreated(viewId); + /// Because _initialize is async, it can happen that [LinkViewController.dispose] + /// may get called before this `then` callback. + /// Check that the `controller` that was created by this factory is not + /// disposed before calling `onPlatformViewCreated`. + if (_instances[viewId] == controller) { + params.onPlatformViewCreated(viewId); + } }); return controller; } @@ -159,9 +164,6 @@ class LinkViewController extends PlatformViewController { @override final int viewId; - /// The context of the [Link] widget that created this controller. - final BuildContext context; - late html.Element _element; bool get _isInitialized => _element != null; @@ -208,7 +210,7 @@ class LinkViewController extends PlatformViewController { // browser handle it. event.preventDefault(); final String routeName = _uri.toString(); - pushRouteNameToFramework(context, routeName); + pushRouteNameToFramework(null, routeName); } Uri? _uri; diff --git a/packages/url_launcher/url_launcher_web/lib/src/shims/dart_ui_fake.dart b/packages/url_launcher/url_launcher_web/lib/src/shims/dart_ui_fake.dart index f51dce946acc..ec46f2789ab5 100644 --- a/packages/url_launcher/url_launcher_web/lib/src/shims/dart_ui_fake.dart +++ b/packages/url_launcher/url_launcher_web/lib/src/shims/dart_ui_fake.dart @@ -11,10 +11,10 @@ import 'dart:html' as html; // ignore_for_file: camel_case_types /// Shim for web_ui engine.PlatformViewRegistry -/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62 +/// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/ui.dart#L62 class platformViewRegistry { /// Shim for registerViewFactory - /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72 + /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/ui.dart#L72 static bool registerViewFactory( String viewTypeId, html.Element Function(int viewId) viewFactory, {bool isVisible = true}) { @@ -23,10 +23,10 @@ class platformViewRegistry { } /// Shim for web_ui engine.AssetManager. -/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12 +/// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L12 class webOnlyAssetManager { /// Shim for getAssetUrl. - /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L45 + /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L45 static String getAssetUrl(String asset) => ''; } diff --git a/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart b/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart index 72540c3c3b80..636cd8c513a3 100644 --- a/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart +++ b/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart @@ -64,6 +64,7 @@ class UrlLauncherPlugin extends UrlLauncherPlatform { // See https://github.com/flutter/flutter/issues/51461 for reference. final String target = webOnlyWindowName ?? ((_isSafari && _isSafariTargetTopScheme(url)) ? '_top' : ''); + // ignore: unsafe_html return _window.open(url, target); } diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index c45c062255ad..6d4c80689427 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -2,7 +2,7 @@ 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.9 +version: 2.0.13 environment: sdk: ">=2.12.0 <3.0.0" @@ -21,7 +21,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - url_launcher_platform_interface: ^2.0.0 + url_launcher_platform_interface: ^2.0.3 dev_dependencies: flutter_test: diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md index e02f5a2288e1..a5952feb4978 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 3.0.1 + +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + ## 3.0.0 * Changes the major version since, due to a typo in `default_package` in diff --git a/packages/url_launcher/url_launcher_windows/example/README.md b/packages/url_launcher/url_launcher_windows/example/README.md index e444852697b9..96b8bb17dbff 100644 --- a/packages/url_launcher/url_launcher_windows/example/README.md +++ b/packages/url_launcher/url_launcher_windows/example/README.md @@ -1,3 +1,9 @@ -# url_launcher_windows_example +# Platform Implementation Test App -Demonstrates the url_launcher_windows plugin. +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/url_launcher/url_launcher_windows/example/lib/main.dart b/packages/url_launcher/url_launcher_windows/example/lib/main.dart index a9a5d22dadd5..0b985e78ac0d 100644 --- a/packages/url_launcher/url_launcher_windows/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_windows/example/lib/main.dart @@ -9,10 +9,12 @@ import 'package:flutter/material.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -30,7 +32,7 @@ class MyHomePage extends StatefulWidget { final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { diff --git a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml index 08350fdaab65..966d32c779e8 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: ">=1.20.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -21,6 +21,8 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugins.cmake b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugins.cmake index 411af46dd721..88b22e5c775e 100644 --- a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugins.cmake +++ b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST url_launcher_windows ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index 95f17ad9e921..b35b62e1d82e 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.0 +version: 3.0.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index ee0accb22c79..0885f28f9db8 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,50 @@ +## NEXT + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. + +## 2.4.7 + +* Updates README via code excerpts. +* Fixes violations of new analysis option use_named_constants. + +## 2.4.6 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 2.4.5 + +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/104231). +* Fixes an exception when a disposed VideoPlayerController is disposed again. + +## 2.4.4 + +* Updates references to the obsolete master branch. + +## 2.4.3 + +* Fixes Android to correctly display videos recorded in landscapeRight ([#60327](https://github.com/flutter/flutter/issues/60327)). +* Fixes order-dependent unit tests. + +## 2.4.2 + +* Minor fixes for new analysis options. + +## 2.4.1 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 2.4.0 + +* Updates minimum Flutter version to 2.10. +* Adds OS version support information to README. +* Adds `setClosedCaptionFile` method to `VideoPlayerController`. + +## 2.3.0 + +* Adds `allowBackgroundPlayback` to `VideoPlayerOptions`. + ## 2.2.19 * Internal code cleanup for stricter analysis options. diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md index 84b3ae7dfb54..de10f1e2f1dd 100644 --- a/packages/video_player/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -1,10 +1,16 @@ + + # Video Player plugin for Flutter [![pub package](https://img.shields.io/pub/v/video_player.svg)](https://pub.dev/packages/video_player) A Flutter plugin for iOS, Android and Web for playing back video on a Widget surface. -![The example app running in iOS](https://github.com/flutter/plugins/blob/master/packages/video_player/video_player/doc/demo_ipod.gif?raw=true) +| | Android | iOS | Web | +|-------------|---------|------|-------| +| **Support** | SDK 16+ | 9.0+ | Any\* | + +![The example app running in iOS](https://github.com/flutter/plugins/blob/main/packages/video_player/video_player/doc/demo_ipod.gif?raw=true) ## Installation @@ -31,7 +37,7 @@ Android Manifest file, located in `/android/app/src/main/AndroidMa > The Web platform does **not** suppport `dart:io`, so avoid using the `VideoPlayerController.file` constructor for the plugin. Using the constructor attempts to create a `VideoPlayerController.file` that will throw an `UnimplementedError`. -Different web browsers may have different video-playback capabilities (supported formats, autoplay...). Check [package:video_player_web](https://pub.dev/packages/video_player_web) for more web-specific information. +\* Different web browsers may have different video-playback capabilities (supported formats, autoplay...). Check [package:video_player_web](https://pub.dev/packages/video_player_web) for more web-specific information. The `VideoPlayerOptions.mixWithOthers` option can't be implemented in web, at least at the moment. If you use this option in web it will be silently ignored. @@ -46,19 +52,23 @@ The `VideoPlayerOptions.mixWithOthers` option can't be implemented in web, at le ## Example + ```dart -import 'package:video_player/video_player.dart'; import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; -void main() => runApp(VideoApp()); +void main() => runApp(const VideoApp()); +/// Stateful widget to fetch and then display video content. class VideoApp extends StatefulWidget { + const VideoApp({Key? key}) : super(key: key); + @override _VideoAppState createState() => _VideoAppState(); } class _VideoAppState extends State { - VideoPlayerController _controller; + late VideoPlayerController _controller; @override void initState() { @@ -119,7 +129,7 @@ This is not complete as of now. You can contribute to this section by [opening a You can set the playback speed on your `_controller` (instance of `VideoPlayerController`) by calling `_controller.setPlaybackSpeed`. `setPlaybackSpeed` takes a `double` speed value indicating -the rate of playback for your video. +the rate of playback for your video. For example, when given a value of `2.0`, your video will play at 2x the regular playback speed and so on. diff --git a/packages/video_player/video_player/example/README.md b/packages/video_player/video_player/example/README.md index 8ceb0ff485fa..f5974e947c00 100644 --- a/packages/video_player/video_player/example/README.md +++ b/packages/video_player/video_player/example/README.md @@ -1,8 +1,3 @@ # video_player_example Demonstrates how to use the video_player plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). 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 7b3c7db80c7e..338eeb8944f7 100644 --- a/packages/video_player/video_player/example/android/app/build.gradle +++ b/packages/video_player/video_player/example/android/app/build.gradle @@ -58,9 +58,9 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.13' - testImplementation 'org.robolectric:robolectric:4.4' - testImplementation 'org.mockito:mockito-core:3.5.13' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.robolectric:robolectric:4.8.1' + testImplementation 'org.mockito:mockito-core:4.7.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/app/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/video_player/video_player/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/video_player/video_player/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/video_player/video_player/example/build.excerpt.yaml b/packages/video_player/video_player/example/build.excerpt.yaml new file mode 100644 index 000000000000..c9a9c71ba14f --- /dev/null +++ b/packages/video_player/video_player/example/build.excerpt.yaml @@ -0,0 +1,20 @@ +targets: + $default: + sources: + include: + - lib/** + - android/app/src/main/** + # Some default includes that aren't really used here but will prevent + # false-negative warnings: + - $package$ + - lib/$lib$ + exclude: + - '**/.*/**' + - '**/build/**' + - 'android/app/src/main/res/**' + builders: + code_excerpter|code_excerpter: + enabled: true + generate_for: + - '**/*.dart' + - android/**/*.xml diff --git a/packages/video_player/video_player/example/integration_test/controller_swap_test.dart b/packages/video_player/video_player/example/integration_test/controller_swap_test.dart index f80e600bcff5..bdae599ebc8b 100644 --- a/packages/video_player/video_player/example/integration_test/controller_swap_test.dart +++ b/packages/video_player/video_player/example/integration_test/controller_swap_test.dart @@ -65,7 +65,7 @@ void main() { // Expect that `another` played. expect(another.value.position, - (Duration position) => position > const Duration(seconds: 0)); + (Duration position) => position > Duration.zero); await expectLater(started.future, completes); await expectLater(ended.future, completes); @@ -76,7 +76,6 @@ void main() { Widget renderVideoWidget(VideoPlayerController controller) { return Material( - elevation: 0, child: Directionality( textDirection: TextDirection.ltr, child: Center( diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart index 746c63fcbfd6..dd77a2f0252a 100644 --- a/packages/video_player/video_player/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart @@ -4,6 +4,8 @@ import 'dart:async'; import 'dart:io'; +// 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:flutter/foundation.dart'; @@ -21,10 +23,13 @@ const String _videoAssetKey = kIsWeb ? 'assets/Butterfly-209.webm' : 'assets/Butterfly-209.mp4'; // Returns the URL to load an asset from this example app as a network source. +// +// TODO(stuartmorgan): Convert this to a local `HttpServer` that vends the +// assets directly, https://github.com/flutter/flutter/issues/95420 String getUrlForAssetAsNetworkSource(String assetKey) { return 'https://github.com/flutter/plugins/blob/' // This hash can be rolled forward to pick up newly-added assets. - 'cba393233e559c925a4daf71b06b4bb01c606762' + 'cb381ced070d356799dddf24aca38ce0579d3d7b' '/packages/video_player/video_player/example/' '$assetKey' '?raw=true'; @@ -32,22 +37,22 @@ String getUrlForAssetAsNetworkSource(String assetKey) { void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - late VideoPlayerController _controller; - tearDown(() async => _controller.dispose()); + late VideoPlayerController controller; + tearDown(() async => controller.dispose()); group('asset videos', () { setUp(() { - _controller = VideoPlayerController.asset(_videoAssetKey); + controller = VideoPlayerController.asset(_videoAssetKey); }); testWidgets('can be initialized', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); - expect(_controller.value.isInitialized, true); - expect(_controller.value.position, const Duration(seconds: 0)); - expect(_controller.value.isPlaying, false); + expect(controller.value.isInitialized, true); + expect(controller.value.position, Duration.zero); + expect(controller.value.isPlaying, false); // The WebM version has a slightly different duration than the MP4. - expect(_controller.value.duration, + expect(controller.value.duration, const Duration(seconds: 7, milliseconds: kIsWeb ? 544 : 540)); }); @@ -56,7 +61,7 @@ void main() { (WidgetTester tester) async { final VideoPlayerController networkController = VideoPlayerController.network( - 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8', + 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8', ); await networkController.initialize(); @@ -72,117 +77,116 @@ void main() { testWidgets( 'can be played', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); // Mute to allow playing without DOM interaction on Web. // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes - await _controller.setVolume(0); + await controller.setVolume(0); - await _controller.play(); + await controller.play(); await tester.pumpAndSettle(_playDuration); - expect(_controller.value.isPlaying, true); - expect(_controller.value.position, - (Duration position) => position > const Duration(seconds: 0)); + expect(controller.value.isPlaying, true); + expect(controller.value.position, + (Duration position) => position > Duration.zero); }, ); testWidgets( 'can seek', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); - await _controller.seekTo(const Duration(seconds: 3)); + await controller.seekTo(const Duration(seconds: 3)); - expect(_controller.value.position, const Duration(seconds: 3)); + expect(controller.value.position, const Duration(seconds: 3)); }, ); testWidgets( 'can be paused', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); // Mute to allow playing without DOM interaction on Web. // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes - await _controller.setVolume(0); + await controller.setVolume(0); // Play for a second, then pause, and then wait a second. - await _controller.play(); + await controller.play(); await tester.pumpAndSettle(_playDuration); - await _controller.pause(); - final Duration pausedPosition = _controller.value.position; + await controller.pause(); + final Duration pausedPosition = controller.value.position; await tester.pumpAndSettle(_playDuration); // Verify that we stopped playing after the pause. - expect(_controller.value.isPlaying, false); - expect(_controller.value.position, pausedPosition); + expect(controller.value.isPlaying, false); + expect(controller.value.position, pausedPosition); }, ); testWidgets( 'stay paused when seeking after video completed', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); // Mute to allow playing without DOM interaction on Web. // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes - await _controller.setVolume(0); + await controller.setVolume(0); final Duration tenMillisBeforeEnd = - _controller.value.duration - const Duration(milliseconds: 10); - await _controller.seekTo(tenMillisBeforeEnd); - await _controller.play(); + controller.value.duration - const Duration(milliseconds: 10); + await controller.seekTo(tenMillisBeforeEnd); + await controller.play(); await tester.pumpAndSettle(_playDuration); - expect(_controller.value.isPlaying, false); - expect(_controller.value.position, _controller.value.duration); + expect(controller.value.isPlaying, false); + expect(controller.value.position, controller.value.duration); - await _controller.seekTo(tenMillisBeforeEnd); + await controller.seekTo(tenMillisBeforeEnd); await tester.pumpAndSettle(_playDuration); - expect(_controller.value.isPlaying, false); - expect(_controller.value.position, tenMillisBeforeEnd); + expect(controller.value.isPlaying, false); + expect(controller.value.position, tenMillisBeforeEnd); }, ); testWidgets( 'do not exceed duration on play after video completed', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); // Mute to allow playing without DOM interaction on Web. // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes - await _controller.setVolume(0); - await _controller.seekTo( - _controller.value.duration - const Duration(milliseconds: 10)); - await _controller.play(); + await controller.setVolume(0); + await controller.seekTo( + controller.value.duration - const Duration(milliseconds: 10)); + await controller.play(); await tester.pumpAndSettle(_playDuration); - expect(_controller.value.isPlaying, false); - expect(_controller.value.position, _controller.value.duration); + expect(controller.value.isPlaying, false); + expect(controller.value.position, controller.value.duration); - await _controller.play(); + await controller.play(); await tester.pumpAndSettle(_playDuration); - expect(_controller.value.position, - lessThanOrEqualTo(_controller.value.duration)); + expect(controller.value.position, + lessThanOrEqualTo(controller.value.duration)); }, ); testWidgets('test video player view with local asset', (WidgetTester tester) async { Future started() async { - await _controller.initialize(); - await _controller.play(); + await controller.initialize(); + await controller.play(); return true; } await tester.pumpWidget(Material( - elevation: 0, child: Directionality( textDirection: TextDirection.ltr, child: Center( child: FutureBuilder( future: started(), builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.data == true) { + if (snapshot.data ?? false) { return AspectRatio( - aspectRatio: _controller.value.aspectRatio, - child: VideoPlayer(_controller), + aspectRatio: controller.value.aspectRatio, + child: VideoPlayer(controller), ); } else { return const Text('waiting for video to load'); @@ -194,7 +198,7 @@ void main() { )); await tester.pumpAndSettle(); - expect(_controller.value.isPlaying, true); + expect(controller.value.isPlaying, true); }, skip: kIsWeb || // Web does not support local assets. // Extremely flaky on iOS: https://github.com/flutter/flutter/issues/86915 @@ -212,60 +216,55 @@ void main() { final File file = File('$tempDir/$filename'); await file.writeAsBytes(bytes.buffer.asInt8List()); - _controller = VideoPlayerController.file(file); + controller = VideoPlayerController.file(file); }); testWidgets('test video player using static file() method as constructor', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); - await _controller.play(); - expect(_controller.value.isPlaying, true); + await controller.play(); + expect(controller.value.isPlaying, true); - await _controller.pause(); - expect(_controller.value.isPlaying, false); + await controller.pause(); + expect(controller.value.isPlaying, false); }, skip: kIsWeb); }); group('network videos', () { setUp(() { - // TODO(stuartmorgan): Remove this conditional and update the hash in - // getUrlForAssetAsNetworkSource as a follow-up, once the webm asset is - // checked in. - final String videoUrl = kIsWeb - ? 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm' - : getUrlForAssetAsNetworkSource(_videoAssetKey); - _controller = VideoPlayerController.network(videoUrl); + controller = VideoPlayerController.network( + getUrlForAssetAsNetworkSource(_videoAssetKey)); }); testWidgets( 'reports buffering status', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); // Mute to allow playing without DOM interaction on Web. // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes - await _controller.setVolume(0); + await controller.setVolume(0); final Completer started = Completer(); final Completer ended = Completer(); - _controller.addListener(() { - if (!started.isCompleted && _controller.value.isBuffering) { + controller.addListener(() { + if (!started.isCompleted && controller.value.isBuffering) { started.complete(); } if (started.isCompleted && - !_controller.value.isBuffering && + !controller.value.isBuffering && !ended.isCompleted) { ended.complete(); } }); - await _controller.play(); - await _controller.seekTo(const Duration(seconds: 5)); + await controller.play(); + await controller.seekTo(const Duration(seconds: 5)); await tester.pumpAndSettle(_playDuration); - await _controller.pause(); + await controller.pause(); - expect(_controller.value.isPlaying, false); - expect(_controller.value.position, - (Duration position) => position > const Duration(seconds: 0)); + expect(controller.value.isPlaying, false); + expect(controller.value.position, + (Duration position) => position > Duration.zero); await expectLater(started.future, completes); await expectLater(ended.future, completes); @@ -278,63 +277,63 @@ void main() { // but could be removed in the future. group('asset audios', () { setUp(() { - _controller = VideoPlayerController.asset('assets/Audio.mp3'); + controller = VideoPlayerController.asset('assets/Audio.mp3'); }); testWidgets('can be initialized', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); - expect(_controller.value.isInitialized, true); - expect(_controller.value.position, const Duration(seconds: 0)); - expect(_controller.value.isPlaying, false); - // Due to the duration calculation accurancy between platforms, + expect(controller.value.isInitialized, true); + expect(controller.value.position, Duration.zero); + expect(controller.value.isPlaying, false); + // Due to the duration calculation accuracy between platforms, // the milliseconds on Web will be a slightly different from natives. // The audio was made with 44100 Hz, 192 Kbps CBR, and 32 bits. expect( - _controller.value.duration, + controller.value.duration, const Duration(seconds: 5, milliseconds: kIsWeb ? 42 : 41), ); }); testWidgets('can be played', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); // Mute to allow playing without DOM interaction on Web. // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes - await _controller.setVolume(0); + await controller.setVolume(0); - await _controller.play(); + await controller.play(); await tester.pumpAndSettle(_playDuration); - expect(_controller.value.isPlaying, true); + expect(controller.value.isPlaying, true); expect( - _controller.value.position, - (Duration position) => position > const Duration(milliseconds: 0), + controller.value.position, + (Duration position) => position > Duration.zero, ); }); testWidgets('can seek', (WidgetTester tester) async { - await _controller.initialize(); - await _controller.seekTo(const Duration(seconds: 3)); + await controller.initialize(); + await controller.seekTo(const Duration(seconds: 3)); - expect(_controller.value.position, const Duration(seconds: 3)); + expect(controller.value.position, const Duration(seconds: 3)); }); testWidgets('can be paused', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); // Mute to allow playing without DOM interaction on Web. // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes - await _controller.setVolume(0); + await controller.setVolume(0); // Play for a second, then pause, and then wait a second. - await _controller.play(); + await controller.play(); await tester.pumpAndSettle(_playDuration); - await _controller.pause(); - final Duration pausedPosition = _controller.value.position; + await controller.pause(); + final Duration pausedPosition = controller.value.position; await tester.pumpAndSettle(_playDuration); // Verify that we stopped playing after the pause. - expect(_controller.value.isPlaying, false); - expect(_controller.value.position, pausedPosition); + expect(controller.value.isPlaying, false); + expect(controller.value.position, pausedPosition); }); }); } diff --git a/packages/video_player/video_player/example/lib/basic.dart b/packages/video_player/video_player/example/lib/basic.dart new file mode 100644 index 000000000000..169f1cdd00a8 --- /dev/null +++ b/packages/video_player/video_player/example/lib/basic.dart @@ -0,0 +1,73 @@ +// 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 used to extract code samples for the README.md file. +// Run update-excerpts if you modify this file. + +// ignore_for_file: library_private_types_in_public_api, public_member_api_docs + +// #docregion basic-example +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +void main() => runApp(const VideoApp()); + +/// Stateful widget to fetch and then display video content. +class VideoApp extends StatefulWidget { + const VideoApp({Key? key}) : super(key: key); + + @override + _VideoAppState createState() => _VideoAppState(); +} + +class _VideoAppState extends State { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4') + ..initialize().then((_) { + // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Video Demo', + home: Scaffold( + body: Center( + child: _controller.value.isInitialized + ? AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ) + : Container(), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + setState(() { + _controller.value.isPlaying + ? _controller.pause() + : _controller.play(); + }); + }, + child: Icon( + _controller.value.isPlaying ? Icons.pause : Icons.play_arrow, + ), + ), + ), + ); + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + } +} +// #enddocregion basic-example diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart index 5d496a9f5e7d..208cd2fc6c39 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -7,7 +7,6 @@ /// An example of using the plugin, controlling lifecycle and playback of the /// video. -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; @@ -274,7 +273,7 @@ class _ControlsOverlay extends StatelessWidget { Duration(seconds: -3), Duration(seconds: -1, milliseconds: -500), Duration(milliseconds: -250), - Duration(milliseconds: 0), + Duration.zero, Duration(milliseconds: 250), Duration(seconds: 1, milliseconds: 500), Duration(seconds: 3), @@ -420,12 +419,11 @@ class _PlayerVideoAndPopPageState extends State<_PlayerVideoAndPopPage> { @override Widget build(BuildContext context) { return Material( - elevation: 0, child: Center( child: FutureBuilder( future: started(), builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.data == true) { + if (snapshot.data ?? false) { return AspectRatio( aspectRatio: _videoPlayerController.value.aspectRatio, child: VideoPlayer(_videoPlayerController), diff --git a/packages/video_player/video_player/example/pubspec.yaml b/packages/video_player/video_player/example/pubspec.yaml index 6032f3ceed65..7b6aa09329fa 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: ">=1.12.13+hotfix.5" + flutter: ">=2.10.0" dependencies: flutter: @@ -18,6 +18,7 @@ dependencies: path: ../ dev_dependencies: + build_runner: ^2.1.10 flutter_driver: sdk: flutter flutter_test: diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 672ba2efcde3..c1f4886282f8 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -48,6 +49,7 @@ class VideoPlayerValue { this.isBuffering = false, this.volume = 1.0, this.playbackSpeed = 1.0, + this.rotationCorrection = 0, this.errorDescription, }); @@ -111,6 +113,9 @@ class VideoPlayerValue { /// The [size] of the currently loaded video. final Size size; + /// Degrees to rotate the video (clockwise) so it is displayed correctly. + final int rotationCorrection; + /// Indicates whether or not the video has been loaded and is ready to play. final bool isInitialized; @@ -136,7 +141,7 @@ class VideoPlayerValue { } /// Returns a new instance that has the same values as this current instance, - /// except for any overrides passed in as arguments to [copyWidth]. + /// except for any overrides passed in as arguments to [copyWith]. VideoPlayerValue copyWith({ Duration? duration, Size? size, @@ -150,6 +155,7 @@ class VideoPlayerValue { bool? isBuffering, double? volume, double? playbackSpeed, + int? rotationCorrection, String? errorDescription = _defaultErrorDescription, }) { return VideoPlayerValue( @@ -165,6 +171,7 @@ class VideoPlayerValue { isBuffering: isBuffering ?? this.isBuffering, volume: volume ?? this.volume, playbackSpeed: playbackSpeed ?? this.playbackSpeed, + rotationCorrection: rotationCorrection ?? this.rotationCorrection, errorDescription: errorDescription != _defaultErrorDescription ? errorDescription : this.errorDescription, @@ -207,8 +214,11 @@ class VideoPlayerController extends ValueNotifier { /// null. The [package] argument must be non-null when the asset comes from a /// package and null otherwise. VideoPlayerController.asset(this.dataSource, - {this.package, this.closedCaptionFile, this.videoPlayerOptions}) - : dataSourceType = DataSourceType.asset, + {this.package, + Future? closedCaptionFile, + this.videoPlayerOptions}) + : _closedCaptionFileFuture = closedCaptionFile, + dataSourceType = DataSourceType.asset, formatHint = null, httpHeaders = const {}, super(VideoPlayerValue(duration: Duration.zero)); @@ -225,10 +235,11 @@ class VideoPlayerController extends ValueNotifier { VideoPlayerController.network( this.dataSource, { this.formatHint, - this.closedCaptionFile, + Future? closedCaptionFile, this.videoPlayerOptions, this.httpHeaders = const {}, - }) : dataSourceType = DataSourceType.network, + }) : _closedCaptionFileFuture = closedCaptionFile, + dataSourceType = DataSourceType.network, package = null, super(VideoPlayerValue(duration: Duration.zero)); @@ -237,8 +248,9 @@ class VideoPlayerController extends ValueNotifier { /// This will load the file from the file-URI given by: /// `'file://${file.path}'`. VideoPlayerController.file(File file, - {this.closedCaptionFile, this.videoPlayerOptions}) - : dataSource = 'file://${file.path}', + {Future? closedCaptionFile, this.videoPlayerOptions}) + : _closedCaptionFileFuture = closedCaptionFile, + dataSource = 'file://${file.path}', dataSourceType = DataSourceType.file, package = null, formatHint = null, @@ -250,9 +262,10 @@ class VideoPlayerController extends ValueNotifier { /// This will load the video from the input content-URI. /// This is supported on Android only. VideoPlayerController.contentUri(Uri contentUri, - {this.closedCaptionFile, this.videoPlayerOptions}) + {Future? closedCaptionFile, this.videoPlayerOptions}) : assert(defaultTargetPlatform == TargetPlatform.android, 'VideoPlayerController.contentUri is only supported on Android.'), + _closedCaptionFileFuture = closedCaptionFile, dataSource = contentUri.toString(), dataSourceType = DataSourceType.contentUri, package = null, @@ -283,19 +296,13 @@ class VideoPlayerController extends ValueNotifier { /// Only set for [asset] videos. The package that the asset was loaded from. final String? package; - /// Optional field to specify a file containing the closed - /// captioning. - /// - /// This future will be awaited and the file will be loaded when - /// [initialize()] is called. - final Future? closedCaptionFile; - + Future? _closedCaptionFileFuture; ClosedCaptionFile? _closedCaptionFile; Timer? _timer; bool _isDisposed = false; Completer? _creatingCompleter; StreamSubscription? _eventSubscription; - late _VideoAppLifeCycleObserver _lifeCycleObserver; + _VideoAppLifeCycleObserver? _lifeCycleObserver; /// The id of a texture that hasn't been initialized. @visibleForTesting @@ -309,8 +316,12 @@ class VideoPlayerController extends ValueNotifier { /// Attempts to open the given [dataSource] and load metadata about the video. Future initialize() async { - _lifeCycleObserver = _VideoAppLifeCycleObserver(this); - _lifeCycleObserver.initialize(); + final bool allowBackgroundPlayback = + videoPlayerOptions?.allowBackgroundPlayback ?? false; + if (!allowBackgroundPlayback) { + _lifeCycleObserver = _VideoAppLifeCycleObserver(this); + } + _lifeCycleObserver?.initialize(); _creatingCompleter = Completer(); late DataSource dataSourceDescription; @@ -364,6 +375,7 @@ class VideoPlayerController extends ValueNotifier { value = value.copyWith( duration: event.duration, size: event.size, + rotationCorrection: event.rotationCorrection, isInitialized: event.duration != null, errorDescription: null, ); @@ -393,9 +405,8 @@ class VideoPlayerController extends ValueNotifier { } } - if (closedCaptionFile != null) { - _closedCaptionFile ??= await closedCaptionFile; - value = value.copyWith(caption: _getCaptionAt(value.position)); + if (_closedCaptionFileFuture != null) { + await _updateClosedCaptionWithFuture(_closedCaptionFileFuture); } void errorListener(Object obj) { @@ -415,6 +426,10 @@ class VideoPlayerController extends ValueNotifier { @override Future dispose() async { + if (_isDisposed) { + return; + } + if (_creatingCompleter != null) { await _creatingCompleter!.future; if (!_isDisposed) { @@ -423,7 +438,7 @@ class VideoPlayerController extends ValueNotifier { await _eventSubscription?.cancel(); await _videoPlayerPlatform.dispose(_textureId); } - _lifeCycleObserver.dispose(); + _lifeCycleObserver?.dispose(); } _isDisposed = true; super.dispose(); @@ -438,7 +453,7 @@ class VideoPlayerController extends ValueNotifier { /// finished. Future play() async { if (value.position == value.duration) { - await seekTo(const Duration()); + await seekTo(Duration.zero); } value = value.copyWith(isPlaying: true); await _applyPlayPause(); @@ -541,8 +556,8 @@ class VideoPlayerController extends ValueNotifier { } if (position > value.duration) { position = value.duration; - } else if (position < const Duration()) { - position = const Duration(); + } else if (position < Duration.zero) { + position = Duration.zero; } await _videoPlayerPlatform.seekTo(_textureId, position); _updatePosition(position); @@ -630,6 +645,28 @@ class VideoPlayerController extends ValueNotifier { return Caption.none; } + /// Returns the file containing closed captions for the video, if any. + Future? get closedCaptionFile { + return _closedCaptionFileFuture; + } + + /// Sets a closed caption file. + /// + /// If [closedCaptionFile] is null, closed captions will be removed. + Future setClosedCaptionFile( + Future? closedCaptionFile, + ) async { + await _updateClosedCaptionWithFuture(closedCaptionFile); + _closedCaptionFileFuture = closedCaptionFile; + } + + Future _updateClosedCaptionWithFuture( + Future? closedCaptionFile, + ) async { + _closedCaptionFile = await closedCaptionFile; + value = value.copyWith(caption: _getCaptionAt(value.position)); + } + void _updatePosition(Duration position) { value = value.copyWith( position: position, @@ -683,14 +720,14 @@ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { /// Widget that displays the video controlled by [controller]. class VideoPlayer extends StatefulWidget { /// Uses the given [controller] for all video rendered in this widget. - const VideoPlayer(this.controller); + const VideoPlayer(this.controller, {Key? key}) : super(key: key); /// The [VideoPlayerController] responsible for the video being rendered in /// this widget. final VideoPlayerController controller; @override - _VideoPlayerState createState() => _VideoPlayerState(); + State createState() => _VideoPlayerState(); } class _VideoPlayerState extends State { @@ -736,14 +773,33 @@ class _VideoPlayerState extends State { Widget build(BuildContext context) { return _textureId == VideoPlayerController.kUninitializedTextureId ? Container() - : _videoPlayerPlatform.buildView(_textureId); + : _VideoPlayerWithRotation( + rotation: widget.controller.value.rotationCorrection, + child: _videoPlayerPlatform.buildView(_textureId), + ); } } +class _VideoPlayerWithRotation extends StatelessWidget { + const _VideoPlayerWithRotation( + {Key? key, required this.rotation, required this.child}) + : super(key: key); + final int rotation; + final Widget child; + + @override + Widget build(BuildContext context) => rotation == 0 + ? child + : Transform.rotate( + angle: rotation * math.pi / 180, + child: child, + ); +} + /// Used to configure the [VideoProgressIndicator] widget's colors for how it /// describes the video's status. /// -/// The widget uses default colors that are customizeable through this class. +/// The widget uses default colors that are customizable through this class. class VideoProgressColors { /// Any property can be set to any color. They each have defaults. /// @@ -858,10 +914,11 @@ class VideoProgressIndicator extends StatefulWidget { /// to `top: 5.0`. const VideoProgressIndicator( this.controller, { + Key? key, this.colors = const VideoProgressColors(), required this.allowScrubbing, this.padding = const EdgeInsets.only(top: 5.0), - }); + }) : super(key: key); /// The [VideoPlayerController] that actually associates a video with this /// widget. @@ -885,7 +942,7 @@ class VideoProgressIndicator extends StatefulWidget { final EdgeInsets padding; @override - _VideoProgressIndicatorState createState() => _VideoProgressIndicatorState(); + State createState() => _VideoProgressIndicatorState(); } class _VideoProgressIndicatorState extends State { @@ -948,7 +1005,6 @@ class _VideoProgressIndicatorState extends State { ); } else { progressIndicator = LinearProgressIndicator( - value: null, valueColor: AlwaysStoppedAnimation(colors.playedColor), backgroundColor: colors.backgroundColor, ); @@ -959,8 +1015,8 @@ class _VideoProgressIndicatorState extends State { ); if (widget.allowScrubbing) { return _VideoScrubber( - child: paddedProgressIndicator, controller: controller, + child: paddedProgressIndicator, ); } else { return paddedProgressIndicator; diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 2f6e28919d00..7e2df60d3cdd 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.2.19 +version: 2.4.7 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.0" flutter: plugin: @@ -23,9 +23,9 @@ dependencies: flutter: sdk: flutter html: ^0.15.0 - video_player_android: ^2.2.17 + video_player_android: ^2.3.5 video_player_avfoundation: ^2.2.17 - video_player_platform_interface: ">=4.2.0 <6.0.0" + video_player_platform_interface: ^5.1.1 video_player_web: ^2.0.0 dev_dependencies: diff --git a/packages/video_player/video_player/test/closed_caption_file_test.dart b/packages/video_player/video_player/test/closed_caption_file_test.dart index b5c0a8e1db12..a20f9479dc45 100644 --- a/packages/video_player/video_player/test/closed_caption_file_test.dart +++ b/packages/video_player/video_player/test/closed_caption_file_test.dart @@ -21,8 +21,7 @@ void main() { 'number: 1, ' 'start: 0:00:01.000000, ' 'end: 0:00:02.000000, ' - 'text: caption' - ')'); + 'text: caption)'); }); }); } diff --git a/packages/video_player/video_player/test/sub_rip_file_test.dart b/packages/video_player/video_player/test/sub_rip_file_test.dart index ea3bfda036ec..82fe6ce033ab 100644 --- a/packages/video_player/video_player/test/sub_rip_file_test.dart +++ b/packages/video_player/video_player/test/sub_rip_file_test.dart @@ -57,7 +57,7 @@ void main() { ); expect( fourthCaption.text, - '- [ Machinery Beeping ]\n- I\'m not sure what that was,', + "- [ Machinery Beeping ]\n- I'm not sure what that was,", ); }); diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 07afd0f65402..8e5e98b68f18 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -4,11 +4,11 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math' as math; +import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:video_player/video_player.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; @@ -17,6 +17,8 @@ class FakeController extends ValueNotifier implements VideoPlayerController { FakeController() : super(VideoPlayerValue(duration: Duration.zero)); + FakeController.value(VideoPlayerValue value) : super(value); + @override Future dispose() async { super.dispose(); @@ -72,6 +74,11 @@ class FakeController extends ValueNotifier @override void setCaptionOffset(Duration delay) {} + + @override + Future setClosedCaptionFile( + Future? closedCaptionFile, + ) async {} } Future _loadClosedCaption() async => @@ -98,6 +105,19 @@ class _FakeClosedCaptionFile extends ClosedCaptionFile { } void main() { + void verifyPlayStateRespondsToLifecycle( + VideoPlayerController controller, { + required bool shouldPlayInBackground, + }) { + expect(controller.value.isPlaying, true); + _ambiguate(WidgetsBinding.instance)! + .handleAppLifecycleStateChanged(AppLifecycleState.paused); + expect(controller.value.isPlaying, shouldPlayInBackground); + _ambiguate(WidgetsBinding.instance)! + .handleAppLifecycleStateChanged(AppLifecycleState.resumed); + expect(controller.value.isPlaying, true); + } + testWidgets('update texture', (WidgetTester tester) async { final FakeController controller = FakeController(); await tester.pumpWidget(VideoPlayer(controller)); @@ -133,6 +153,35 @@ void main() { findsOneWidget); }); + testWidgets('non-zero rotationCorrection value is used', + (WidgetTester tester) async { + final FakeController controller = FakeController.value( + VideoPlayerValue(duration: Duration.zero, rotationCorrection: 180)); + controller.textureId = 1; + await tester.pumpWidget(VideoPlayer(controller)); + final Transform actualRotationCorrection = + find.byType(Transform).evaluate().single.widget as Transform; + final Float64List actualRotationCorrectionStorage = + actualRotationCorrection.transform.storage; + final Float64List expectedMatrixStorage = + Matrix4.rotationZ(math.pi).storage; + expect(actualRotationCorrectionStorage.length, + equals(expectedMatrixStorage.length)); + for (int i = 0; i < actualRotationCorrectionStorage.length; i++) { + expect(actualRotationCorrectionStorage[i], + moreOrLessEquals(expectedMatrixStorage[i])); + } + }); + + testWidgets('no transform when rotationCorrection is zero', + (WidgetTester tester) async { + final FakeController controller = + FakeController.value(VideoPlayerValue(duration: Duration.zero)); + controller.textureId = 1; + await tester.pumpWidget(VideoPlayer(controller)); + expect(find.byType(Transform), findsNothing); + }); + group('ClosedCaption widget', () { testWidgets('uses a default text style', (WidgetTester tester) async { const String text = 'foo'; @@ -160,8 +209,7 @@ void main() { }); testWidgets('handles null text', (WidgetTester tester) async { - await tester - .pumpWidget(const MaterialApp(home: ClosedCaption(text: null))); + await tester.pumpWidget(const MaterialApp(home: ClosedCaption())); expect(find.byType(Text), findsNothing); }); @@ -194,6 +242,16 @@ void main() { }); group('initialize', () { + test('started app lifecycle observing', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + await controller.play(); + verifyPlayStateRespondsToLifecycle(controller, + shouldPlayInBackground: false); + }); + test('asset', () async { final VideoPlayerController controller = VideoPlayerController.asset( 'a.avi', @@ -314,7 +372,7 @@ void main() { ); expect( controller.textureId, VideoPlayerController.kUninitializedTextureId); - expect(await controller.position, const Duration(seconds: 0)); + expect(await controller.position, Duration.zero); await controller.initialize(); await controller.dispose(); @@ -323,6 +381,17 @@ void main() { expect(await controller.position, isNull); }); + test('calling dispose() on disposed controller does not throw', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + + await controller.initialize(); + await controller.dispose(); + + expect(() async => await controller.dispose(), returnsNormally); + }); + test('play', () async { final VideoPlayerController controller = VideoPlayerController.network( 'https://127.0.0.1', @@ -401,7 +470,7 @@ void main() { 'https://127.0.0.1', ); await controller.initialize(); - expect(await controller.position, const Duration(seconds: 0)); + expect(await controller.position, Duration.zero); await controller.seekTo(const Duration(milliseconds: 500)); @@ -424,13 +493,13 @@ void main() { 'https://127.0.0.1', ); await controller.initialize(); - expect(await controller.position, const Duration(seconds: 0)); + expect(await controller.position, Duration.zero); await controller.seekTo(const Duration(seconds: 100)); expect(await controller.position, const Duration(seconds: 1)); await controller.seekTo(const Duration(seconds: -100)); - expect(await controller.position, const Duration(seconds: 0)); + expect(await controller.position, Duration.zero); }); }); @@ -550,7 +619,7 @@ void main() { ); await controller.initialize(); - expect(controller.value.position, const Duration()); + expect(controller.value.position, Duration.zero); expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 100)); @@ -583,7 +652,7 @@ void main() { await controller.initialize(); controller.setCaptionOffset(const Duration(milliseconds: 100)); - expect(controller.value.position, const Duration()); + expect(controller.value.position, Duration.zero); expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 100)); @@ -619,7 +688,7 @@ void main() { await controller.initialize(); controller.setCaptionOffset(const Duration(milliseconds: -100)); - expect(controller.value.position, const Duration()); + expect(controller.value.position, Duration.zero); expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 100)); @@ -649,6 +718,37 @@ void main() { await controller.seekTo(const Duration(milliseconds: 300)); expect(controller.value.caption.text, 'one'); }); + + test('setClosedCaptionFile loads caption file', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + + await controller.initialize(); + expect(controller.closedCaptionFile, null); + + await controller.setClosedCaptionFile(_loadClosedCaption()); + expect( + (await controller.closedCaptionFile)!.captions, + (await _loadClosedCaption()).captions, + ); + }); + + test('setClosedCaptionFile removes/changes caption file', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + closedCaptionFile: _loadClosedCaption(), + ); + + await controller.initialize(); + expect( + (await controller.closedCaptionFile)!.captions, + (await _loadClosedCaption()).captions, + ); + + await controller.setClosedCaptionFile(null); + expect(controller.closedCaptionFile, null); + }); }); group('Platform callbacks', () { @@ -688,7 +788,7 @@ void main() { await tester.pumpAndSettle(); expect(controller.value.isBuffering, isTrue); - const Duration bufferStart = Duration(seconds: 0); + const Duration bufferStart = Duration.zero; const Duration bufferEnd = Duration(milliseconds: 500); fakeVideoEventStream.add(VideoEvent( eventType: VideoEventType.bufferingUpdate, @@ -783,7 +883,7 @@ void main() { text: 'foo', number: 0, start: Duration.zero, end: Duration.zero); const Duration captionOffset = Duration(milliseconds: 250); final List buffered = [ - DurationRange(const Duration(seconds: 0), const Duration(seconds: 4)) + DurationRange(Duration.zero, const Duration(seconds: 4)) ]; const bool isInitialized = true; const bool isPlaying = true; @@ -900,6 +1000,51 @@ void main() { }); }); + group('VideoPlayerOptions', () { + late FakeVideoPlayerPlatform fakeVideoPlayerPlatform; + + setUp(() { + fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); + VideoPlayerPlatform.instance = fakeVideoPlayerPlatform; + }); + + test('setMixWithOthers', () async { + final VideoPlayerController controller = VideoPlayerController.file( + File(''), + videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true)); + await controller.initialize(); + expect(controller.videoPlayerOptions!.mixWithOthers, true); + }); + + test('true allowBackgroundPlayback continues playback', () async { + final VideoPlayerController controller = VideoPlayerController.file( + File(''), + videoPlayerOptions: VideoPlayerOptions( + allowBackgroundPlayback: true, + ), + ); + await controller.initialize(); + await controller.play(); + verifyPlayStateRespondsToLifecycle( + controller, + shouldPlayInBackground: true, + ); + }); + + test('false allowBackgroundPlayback pauses playback', () async { + final VideoPlayerController controller = VideoPlayerController.file( + File(''), + videoPlayerOptions: VideoPlayerOptions(), + ); + await controller.initialize(); + await controller.play(); + verifyPlayStateRespondsToLifecycle( + controller, + shouldPlayInBackground: false, + ); + }); + }); + test('VideoProgressColors', () { const Color playedColor = Color.fromRGBO(0, 0, 255, 0.75); const Color bufferedColor = Color.fromRGBO(0, 255, 0, 0.5); @@ -914,14 +1059,6 @@ void main() { expect(colors.bufferedColor, bufferedColor); expect(colors.backgroundColor, backgroundColor); }); - - test('setMixWithOthers', () async { - final VideoPlayerController controller = VideoPlayerController.file( - File(''), - videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true)); - await controller.initialize(); - expect(controller.videoPlayerOptions!.mixWithOthers, true); - }); } class FakeVideoPlayerPlatform extends VideoPlayerPlatform { @@ -981,7 +1118,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { @override Future getPosition(int textureId) async { calls.add('position'); - return _positions[textureId] ?? const Duration(seconds: 0); + return _positions[textureId] ?? Duration.zero; } @override @@ -1010,3 +1147,9 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { calls.add('setMixWithOthers'); } } + +/// 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/video_player/video_player/test/web_vtt_test.dart b/packages/video_player/video_player/test/web_vtt_test.dart index bde629219484..b7a7bb51ce2b 100644 --- a/packages/video_player/video_player/test/web_vtt_test.dart +++ b/packages/video_player/video_player/test/web_vtt_test.dart @@ -38,8 +38,7 @@ void main() { expect(parsedFile.captions[0].start, const Duration(seconds: 5, milliseconds: 200)); - expect(parsedFile.captions[0].end, - const Duration(seconds: 6, milliseconds: 000)); + expect(parsedFile.captions[0].end, const Duration(seconds: 6)); expect(parsedFile.captions[0].text, "You know I'm so excited my glasses are falling off here."); }); @@ -106,7 +105,7 @@ void main() { final Caption firstCaption = parsedFile.captions.single; expect(firstCaption.number, 1); expect(firstCaption.start, const Duration(seconds: 13)); - expect(firstCaption.end, const Duration(seconds: 16, milliseconds: 0)); + expect(firstCaption.end, const Duration(seconds: 16)); expect(firstCaption.text, 'Valid'); }); } diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index ec526ee1c7c9..7298bacf4f78 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,51 @@ +## NEXT + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. +* Updates minimum Flutter version to 2.10. +* Fixes violations of new analysis option use_named_constants. + +## 2.3.9 + +* Updates ExoPlayer to 2.18.1. +* Fixes avoid_redundant_argument_values lint warnings and minor typos. + +## 2.3.8 + +* Updates ExoPlayer to 2.18.0. + +## 2.3.7 + +* Bumps gradle version to 7.2.1. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). + +## 2.3.6 + +* Updates references to the obsolete master branch. + +## 2.3.5 + +* Sets rotationCorrection for videos recorded in landscapeRight (https://github.com/flutter/flutter/issues/60327). + +## 2.3.4 + +* Updates ExoPlayer to 2.17.1. + +## 2.3.3 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 2.3.2 + +* Updates ExoPlayer to 2.17.0. + +## 2.3.1 + +* Renames internal method channels to avoid potential confusion with the + default implementation's method channel. +* Updates Pigeon to 2.0.1. + ## 2.3.0 * Updates Pigeon to ^1.0.16. diff --git a/packages/video_player/video_player_android/CONTRIBUTING.md b/packages/video_player/video_player_android/CONTRIBUTING.md index 8dfec9faf809..e06f2233278b 100644 --- a/packages/video_player/video_player_android/CONTRIBUTING.md +++ b/packages/video_player/video_player_android/CONTRIBUTING.md @@ -25,7 +25,7 @@ Then, run the commands above. When you run `pub get` it should warn you that you're using an override. If you do this, you will need to publish pigeon before you can land the updates to this package, since the CI tests run the analysis using latest published version of -pigeon, not your version or the version on master. +pigeon, not your version or the version on `main`. In either case, the configuration will be obtained automatically from the `pigeons/messages.dart` file (see `ConfigurePigeon` at the top of that file). diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle index 6e6e8c792150..2677050d303b 100644 --- a/packages/video_player/video_player_android/android/build.gradle +++ b/packages/video_player/video_player_android/android/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.2.1' } } @@ -34,6 +34,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { + disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' } @@ -43,15 +44,16 @@ android { } dependencies { - implementation 'com.google.android.exoplayer:exoplayer-core:2.14.1' - implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.1' - implementation 'com.google.android.exoplayer:exoplayer-dash:2.14.1' - implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.14.1' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:3.9.0' + implementation 'com.google.android.exoplayer:exoplayer-core:2.18.1' + implementation 'com.google.android.exoplayer:exoplayer-hls:2.18.1' + implementation 'com.google.android.exoplayer:exoplayer-dash:2.18.1' + 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.robolectric:robolectric:4.8.1' } - testOptions { unitTests.includeAndroidResources = true unitTests.returnDefaultValues = true diff --git a/packages/video_player/video_player_android/android/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player_android/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9f96ce648a03..000000000000 --- a/packages/video_player/video_player_android/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Wed Oct 17 09:04:56 PDT 2018 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java index 0cdf3564d334..6593ebf9c22a 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.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 (v1.0.16), do not edit directly. +// Autogenerated from Pigeon (v2.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.videoplayer; @@ -557,10 +557,10 @@ Map toMap() { } } - private static class VideoPlayerApiCodec extends StandardMessageCodec { - public static final VideoPlayerApiCodec INSTANCE = new VideoPlayerApiCodec(); + private static class AndroidVideoPlayerApiCodec extends StandardMessageCodec { + public static final AndroidVideoPlayerApiCodec INSTANCE = new AndroidVideoPlayerApiCodec(); - private VideoPlayerApiCodec() {} + private AndroidVideoPlayerApiCodec() {} @Override protected Object readValueOfType(byte type, ByteBuffer buffer) { @@ -621,40 +621,45 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ - public interface VideoPlayerApi { + public interface AndroidVideoPlayerApi { void initialize(); - TextureMessage create(CreateMessage msg); + @NonNull + TextureMessage create(@NonNull CreateMessage msg); - void dispose(TextureMessage msg); + void dispose(@NonNull TextureMessage msg); - void setLooping(LoopingMessage msg); + void setLooping(@NonNull LoopingMessage msg); - void setVolume(VolumeMessage msg); + void setVolume(@NonNull VolumeMessage msg); - void setPlaybackSpeed(PlaybackSpeedMessage msg); + void setPlaybackSpeed(@NonNull PlaybackSpeedMessage msg); - void play(TextureMessage msg); + void play(@NonNull TextureMessage msg); - PositionMessage position(TextureMessage msg); + @NonNull + PositionMessage position(@NonNull TextureMessage msg); - void seekTo(PositionMessage msg); + void seekTo(@NonNull PositionMessage msg); - void pause(TextureMessage msg); + void pause(@NonNull TextureMessage msg); - void setMixWithOthers(MixWithOthersMessage msg); + void setMixWithOthers(@NonNull MixWithOthersMessage msg); - /** The codec used by VideoPlayerApi. */ + /** The codec used by AndroidVideoPlayerApi. */ static MessageCodec getCodec() { - return VideoPlayerApiCodec.INSTANCE; + return AndroidVideoPlayerApiCodec.INSTANCE; } - /** Sets up an instance of `VideoPlayerApi` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { + /** + * Sets up an instance of `AndroidVideoPlayerApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, AndroidVideoPlayerApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.initialize", getCodec()); + binaryMessenger, "dev.flutter.pigeon.AndroidVideoPlayerApi.initialize", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -674,7 +679,7 @@ static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.create", getCodec()); + binaryMessenger, "dev.flutter.pigeon.AndroidVideoPlayerApi.create", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -699,7 +704,7 @@ static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.dispose", getCodec()); + binaryMessenger, "dev.flutter.pigeon.AndroidVideoPlayerApi.dispose", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -724,7 +729,7 @@ static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.setLooping", getCodec()); + binaryMessenger, "dev.flutter.pigeon.AndroidVideoPlayerApi.setLooping", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -749,7 +754,7 @@ static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.setVolume", getCodec()); + binaryMessenger, "dev.flutter.pigeon.AndroidVideoPlayerApi.setVolume", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -774,7 +779,9 @@ static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed", getCodec()); + binaryMessenger, + "dev.flutter.pigeon.AndroidVideoPlayerApi.setPlaybackSpeed", + getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -799,7 +806,7 @@ static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.play", getCodec()); + binaryMessenger, "dev.flutter.pigeon.AndroidVideoPlayerApi.play", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -824,7 +831,7 @@ static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.position", getCodec()); + binaryMessenger, "dev.flutter.pigeon.AndroidVideoPlayerApi.position", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -849,7 +856,7 @@ static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.seekTo", getCodec()); + binaryMessenger, "dev.flutter.pigeon.AndroidVideoPlayerApi.seekTo", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -874,7 +881,7 @@ static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.pause", getCodec()); + binaryMessenger, "dev.flutter.pigeon.AndroidVideoPlayerApi.pause", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -899,7 +906,9 @@ static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers", getCodec()); + binaryMessenger, + "dev.flutter.pigeon.AndroidVideoPlayerApi.setMixWithOthers", + getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 33593267338c..e130c995aa2a 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -11,14 +11,15 @@ import android.net.Uri; import android.view.Surface; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.Listener; -import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; @@ -28,7 +29,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.util.Util; import io.flutter.plugin.common.EventChannel; @@ -45,17 +46,17 @@ final class VideoPlayer { private static final String FORMAT_HLS = "hls"; private static final String FORMAT_OTHER = "other"; - private SimpleExoPlayer exoPlayer; + private ExoPlayer exoPlayer; private Surface surface; private final TextureRegistry.SurfaceTextureEntry textureEntry; - private QueuingEventSink eventSink = new QueuingEventSink(); + private QueuingEventSink eventSink; private final EventChannel eventChannel; - private boolean isInitialized = false; + @VisibleForTesting boolean isInitialized = false; private final VideoPlayerOptions options; @@ -71,11 +72,11 @@ final class VideoPlayer { this.textureEntry = textureEntry; this.options = options; - exoPlayer = new SimpleExoPlayer.Builder(context).build(); + ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build(); Uri uri = Uri.parse(dataSource); - DataSource.Factory dataSourceFactory; + if (isHTTP(uri)) { DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory() @@ -87,14 +88,30 @@ final class VideoPlayer { } dataSourceFactory = httpDataSourceFactory; } else { - dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer"); + dataSourceFactory = new DefaultDataSource.Factory(context); } MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context); + exoPlayer.setMediaSource(mediaSource); exoPlayer.prepare(); - setupVideoPlayer(eventChannel, textureEntry); + setUpVideoPlayer(exoPlayer, new QueuingEventSink()); + } + + // Constructor used to directly test members of this class. + @VisibleForTesting + VideoPlayer( + ExoPlayer exoPlayer, + EventChannel eventChannel, + TextureRegistry.SurfaceTextureEntry textureEntry, + VideoPlayerOptions options, + QueuingEventSink eventSink) { + this.eventChannel = eventChannel; + this.textureEntry = textureEntry; + this.options = options; + + setUpVideoPlayer(exoPlayer, eventSink); } private static boolean isHTTP(Uri uri) { @@ -109,20 +126,20 @@ private MediaSource buildMediaSource( Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) { int type; if (formatHint == null) { - type = Util.inferContentType(uri.getLastPathSegment()); + type = Util.inferContentType(uri); } else { switch (formatHint) { case FORMAT_SS: - type = C.TYPE_SS; + type = C.CONTENT_TYPE_SS; break; case FORMAT_DASH: - type = C.TYPE_DASH; + type = C.CONTENT_TYPE_DASH; break; case FORMAT_HLS: - type = C.TYPE_HLS; + type = C.CONTENT_TYPE_HLS; break; case FORMAT_OTHER: - type = C.TYPE_OTHER; + type = C.CONTENT_TYPE_OTHER; break; default: type = -1; @@ -130,20 +147,20 @@ private MediaSource buildMediaSource( } } switch (type) { - case C.TYPE_SS: + case C.CONTENT_TYPE_SS: return new SsMediaSource.Factory( new DefaultSsChunkSource.Factory(mediaDataSourceFactory), - new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) + new DefaultDataSource.Factory(context, mediaDataSourceFactory)) .createMediaSource(MediaItem.fromUri(uri)); - case C.TYPE_DASH: + case C.CONTENT_TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), - new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) + new DefaultDataSource.Factory(context, mediaDataSourceFactory)) .createMediaSource(MediaItem.fromUri(uri)); - case C.TYPE_HLS: + case C.CONTENT_TYPE_HLS: return new HlsMediaSource.Factory(mediaDataSourceFactory) .createMediaSource(MediaItem.fromUri(uri)); - case C.TYPE_OTHER: + case C.CONTENT_TYPE_OTHER: return new ProgressiveMediaSource.Factory(mediaDataSourceFactory) .createMediaSource(MediaItem.fromUri(uri)); default: @@ -153,8 +170,10 @@ private MediaSource buildMediaSource( } } - private void setupVideoPlayer( - EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry) { + private void setUpVideoPlayer(ExoPlayer exoPlayer, QueuingEventSink eventSink) { + this.exoPlayer = exoPlayer; + this.eventSink = eventSink; + eventChannel.setStreamHandler( new EventChannel.StreamHandler() { @Override @@ -207,7 +226,7 @@ public void onPlaybackStateChanged(final int playbackState) { } @Override - public void onPlayerError(final ExoPlaybackException error) { + public void onPlayerError(final PlaybackException error) { setBuffering(false); if (eventSink != null) { eventSink.error("VideoError", "Video player had error " + error, null); @@ -225,10 +244,10 @@ void sendBufferingUpdate() { eventSink.success(event); } - @SuppressWarnings("deprecation") - private static void setAudioAttributes(SimpleExoPlayer exoPlayer, boolean isMixMode) { + private static void setAudioAttributes(ExoPlayer exoPlayer, boolean isMixMode) { exoPlayer.setAudioAttributes( - new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build(), !isMixMode); + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(), + !isMixMode); } void play() { @@ -265,7 +284,8 @@ long getPosition() { } @SuppressWarnings("SuspiciousNameCombination") - private void sendInitialized() { + @VisibleForTesting + void sendInitialized() { if (isInitialized) { Map event = new HashMap<>(); event.put("event", "initialized"); @@ -283,7 +303,16 @@ private void sendInitialized() { } event.put("width", width); event.put("height", height); + + // Rotating the video with ExoPlayer does not seem to be possible with a Surface, + // so inform the Flutter code that the widget needs to be rotated to prevent + // upside-down playback for videos with rotationDegrees of 180 (other orientations work + // correctly without correction). + if (rotationDegrees == 180) { + event.put("rotationCorrection", rotationDegrees); + } } + eventSink.success(event); } } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 168d90d8a57a..56fabecd3a96 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -12,13 +12,13 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; +import io.flutter.plugins.videoplayer.Messages.AndroidVideoPlayerApi; import io.flutter.plugins.videoplayer.Messages.CreateMessage; import io.flutter.plugins.videoplayer.Messages.LoopingMessage; import io.flutter.plugins.videoplayer.Messages.MixWithOthersMessage; import io.flutter.plugins.videoplayer.Messages.PlaybackSpeedMessage; import io.flutter.plugins.videoplayer.Messages.PositionMessage; import io.flutter.plugins.videoplayer.Messages.TextureMessage; -import io.flutter.plugins.videoplayer.Messages.VideoPlayerApi; import io.flutter.plugins.videoplayer.Messages.VolumeMessage; import io.flutter.view.TextureRegistry; import java.security.KeyManagementException; @@ -27,7 +27,7 @@ import javax.net.ssl.HttpsURLConnection; /** Android platform implementation of the VideoPlayerPlugin. */ -public class VideoPlayerPlugin implements FlutterPlugin, VideoPlayerApi { +public class VideoPlayerPlugin implements FlutterPlugin, AndroidVideoPlayerApi { private static final String TAG = "VideoPlayerPlugin"; private final LongSparseArray videoPlayers = new LongSparseArray<>(); private FlutterState flutterState; @@ -61,7 +61,6 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra @Override public void onAttachedToEngine(FlutterPluginBinding binding) { - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { try { HttpsURLConnection.setDefaultSSLSocketFactory(new CustomSSLSocketFactory()); @@ -241,11 +240,11 @@ private static final class FlutterState { } void startListening(VideoPlayerPlugin methodCallHandler, BinaryMessenger messenger) { - VideoPlayerApi.setup(messenger, methodCallHandler); + AndroidVideoPlayerApi.setup(messenger, methodCallHandler); } void stopListening(BinaryMessenger messenger) { - VideoPlayerApi.setup(messenger, null); + AndroidVideoPlayerApi.setup(messenger, null); } } } diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java new file mode 100644 index 000000000000..2ed11653a4b8 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java @@ -0,0 +1,15 @@ +// 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.videoplayer; + +import org.junit.Test; + +public class VideoPlayerPluginTest { + // This is only a placeholder test and doesn't actually initialize the plugin. + @Test + public void initPluginDoesNotThrow() { + final VideoPlayerPlugin plugin = new VideoPlayerPlugin(); + } +} diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java index ec960b7a4480..194f7905b63a 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -4,12 +4,154 @@ package io.flutter.plugins.videoplayer; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Format; +import io.flutter.plugin.common.EventChannel; +import io.flutter.view.TextureRegistry; +import java.util.HashMap; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +@RunWith(RobolectricTestRunner.class) public class VideoPlayerTest { - // This is only a placeholder test and doesn't actually initialize the plugin. + private ExoPlayer fakeExoPlayer; + private EventChannel fakeEventChannel; + private TextureRegistry.SurfaceTextureEntry fakeSurfaceTextureEntry; + private VideoPlayerOptions fakeVideoPlayerOptions; + private QueuingEventSink fakeEventSink; + + @Captor private ArgumentCaptor> eventCaptor; + + @Before + public void before() { + MockitoAnnotations.openMocks(this); + + fakeExoPlayer = mock(ExoPlayer.class); + fakeEventChannel = mock(EventChannel.class); + fakeSurfaceTextureEntry = mock(TextureRegistry.SurfaceTextureEntry.class); + fakeVideoPlayerOptions = mock(VideoPlayerOptions.class); + fakeEventSink = mock(QueuingEventSink.class); + } + + @Test + public void sendInitializedSendsExpectedEvent_90RotationDegrees() { + VideoPlayer videoPlayer = + new VideoPlayer( + fakeExoPlayer, + fakeEventChannel, + fakeSurfaceTextureEntry, + fakeVideoPlayerOptions, + fakeEventSink); + Format testFormat = + new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(90).build(); + + when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat); + when(fakeExoPlayer.getDuration()).thenReturn(10L); + + videoPlayer.isInitialized = true; + videoPlayer.sendInitialized(); + + verify(fakeEventSink).success(eventCaptor.capture()); + HashMap event = eventCaptor.getValue(); + + assertEquals(event.get("event"), "initialized"); + assertEquals(event.get("duration"), 10L); + assertEquals(event.get("width"), 200); + assertEquals(event.get("height"), 100); + assertEquals(event.get("rotationCorrection"), null); + } + @Test - public void initPluginDoesNotThrow() { - final VideoPlayerPlugin plugin = new VideoPlayerPlugin(); + public void sendInitializedSendsExpectedEvent_270RotationDegrees() { + VideoPlayer videoPlayer = + new VideoPlayer( + fakeExoPlayer, + fakeEventChannel, + fakeSurfaceTextureEntry, + fakeVideoPlayerOptions, + fakeEventSink); + Format testFormat = + new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(270).build(); + + when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat); + when(fakeExoPlayer.getDuration()).thenReturn(10L); + + videoPlayer.isInitialized = true; + videoPlayer.sendInitialized(); + + verify(fakeEventSink).success(eventCaptor.capture()); + HashMap event = eventCaptor.getValue(); + + assertEquals(event.get("event"), "initialized"); + assertEquals(event.get("duration"), 10L); + assertEquals(event.get("width"), 200); + assertEquals(event.get("height"), 100); + assertEquals(event.get("rotationCorrection"), null); + } + + @Test + public void sendInitializedSendsExpectedEvent_0RotationDegrees() { + VideoPlayer videoPlayer = + new VideoPlayer( + fakeExoPlayer, + fakeEventChannel, + fakeSurfaceTextureEntry, + fakeVideoPlayerOptions, + fakeEventSink); + Format testFormat = + new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(0).build(); + + when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat); + when(fakeExoPlayer.getDuration()).thenReturn(10L); + + videoPlayer.isInitialized = true; + videoPlayer.sendInitialized(); + + verify(fakeEventSink).success(eventCaptor.capture()); + HashMap event = eventCaptor.getValue(); + + assertEquals(event.get("event"), "initialized"); + assertEquals(event.get("duration"), 10L); + assertEquals(event.get("width"), 100); + assertEquals(event.get("height"), 200); + assertEquals(event.get("rotationCorrection"), null); + } + + @Test + public void sendInitializedSendsExpectedEvent_180RotationDegrees() { + VideoPlayer videoPlayer = + new VideoPlayer( + fakeExoPlayer, + fakeEventChannel, + fakeSurfaceTextureEntry, + fakeVideoPlayerOptions, + fakeEventSink); + Format testFormat = + new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(180).build(); + + when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat); + when(fakeExoPlayer.getDuration()).thenReturn(10L); + + videoPlayer.isInitialized = true; + videoPlayer.sendInitialized(); + + verify(fakeEventSink).success(eventCaptor.capture()); + HashMap event = eventCaptor.getValue(); + + assertEquals(event.get("event"), "initialized"); + assertEquals(event.get("duration"), 10L); + assertEquals(event.get("width"), 100); + assertEquals(event.get("height"), 200); + assertEquals(event.get("rotationCorrection"), 180); } } diff --git a/packages/video_player/video_player_android/example/README.md b/packages/video_player/video_player_android/example/README.md index 8ceb0ff485fa..96b8bb17dbff 100644 --- a/packages/video_player/video_player_android/example/README.md +++ b/packages/video_player/video_player_android/example/README.md @@ -1,8 +1,9 @@ -# video_player_example +# Platform Implementation Test App -Demonstrates how to use the video_player plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. 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 7b3c7db80c7e..93caaa5c7c61 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 @@ -59,8 +59,8 @@ flutter { dependencies { testImplementation 'junit:junit:4.13' - testImplementation 'org.robolectric:robolectric:4.4' - testImplementation 'org.mockito:mockito-core:3.5.13' + testImplementation 'org.robolectric:robolectric:4.8.2' + testImplementation 'org.mockito:mockito-core:4.7.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/app/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/video_player/video_player_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/video_player/video_player_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/video_player/video_player_android/example/integration_test/video_player_test.dart b/packages/video_player/video_player_android/example/integration_test/video_player_test.dart index b80ed745a6f9..751412c80f43 100644 --- a/packages/video_player/video_player_android/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player_android/example/integration_test/video_player_test.dart @@ -24,10 +24,13 @@ const Duration _playDuration = Duration(seconds: 1); const String _videoAssetKey = 'assets/Butterfly-209.mp4'; // Returns the URL to load an asset from this example app as a network source. +// +// TODO(stuartmorgan): Convert this to a local `HttpServer` that vends the +// assets directly, https://github.com/flutter/flutter/issues/95420 String getUrlForAssetAsNetworkSource(String assetKey) { return 'https://github.com/flutter/plugins/blob/' // This hash can be rolled forward to pick up newly-added assets. - 'cba393233e559c925a4daf71b06b4bb01c606762' + 'cb381ced070d356799dddf24aca38ce0579d3d7b' '/packages/video_player/video_player/example/' '$assetKey' '?raw=true'; @@ -36,12 +39,12 @@ String getUrlForAssetAsNetworkSource(String assetKey) { void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - late MiniController _controller; - tearDown(() async => _controller.dispose()); + late MiniController controller; + tearDown(() async => controller.dispose()); group('asset videos', () { setUp(() { - _controller = MiniController.asset(_videoAssetKey); + controller = MiniController.asset(_videoAssetKey); }); testWidgets('registers expected implementation', @@ -51,45 +54,44 @@ void main() { }); testWidgets('can be initialized', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); - expect(_controller.value.isInitialized, true); - expect(await _controller.position, const Duration(seconds: 0)); - expect(_controller.value.duration, + expect(controller.value.isInitialized, true); + expect(await controller.position, Duration.zero); + expect(controller.value.duration, const Duration(seconds: 7, milliseconds: 540)); }); testWidgets('can be played', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); - await _controller.play(); + await controller.play(); await tester.pumpAndSettle(_playDuration); - expect( - await _controller.position, greaterThan(const Duration(seconds: 0))); + expect(await controller.position, greaterThan(Duration.zero)); }); testWidgets('can seek', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); - await _controller.seekTo(const Duration(seconds: 3)); + await controller.seekTo(const Duration(seconds: 3)); - expect(await _controller.position, const Duration(seconds: 3)); + expect(await controller.position, const Duration(seconds: 3)); }); testWidgets('can be paused', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); // Play for a second, then pause, and then wait a second. - await _controller.play(); + await controller.play(); await tester.pumpAndSettle(_playDuration); - await _controller.pause(); + await controller.pause(); await tester.pumpAndSettle(_playDuration); - final Duration pausedPosition = (await _controller.position)!; + final Duration pausedPosition = (await controller.position)!; await tester.pumpAndSettle(_playDuration); // Verify that we stopped playing after the pause. - expect(await _controller.position, pausedPosition); + expect(await controller.position, pausedPosition); }); }); @@ -104,50 +106,48 @@ void main() { final File file = File('$tempDir/$filename'); await file.writeAsBytes(bytes.buffer.asInt8List()); - _controller = MiniController.file(file); + controller = MiniController.file(file); }); testWidgets('test video player using static file() method as constructor', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); - await _controller.play(); + await controller.play(); await tester.pumpAndSettle(_playDuration); - expect( - await _controller.position, greaterThan(const Duration(seconds: 0))); + expect(await controller.position, greaterThan(Duration.zero)); }); }); group('network videos', () { setUp(() { final String videoUrl = getUrlForAssetAsNetworkSource(_videoAssetKey); - _controller = MiniController.network(videoUrl); + controller = MiniController.network(videoUrl); }); testWidgets('reports buffering status', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); final Completer started = Completer(); final Completer ended = Completer(); - _controller.addListener(() { - if (!started.isCompleted && _controller.value.isBuffering) { + controller.addListener(() { + if (!started.isCompleted && controller.value.isBuffering) { started.complete(); } if (started.isCompleted && - !_controller.value.isBuffering && + !controller.value.isBuffering && !ended.isCompleted) { ended.complete(); } }); - await _controller.play(); - await _controller.seekTo(const Duration(seconds: 5)); + await controller.play(); + await controller.seekTo(const Duration(seconds: 5)); await tester.pumpAndSettle(_playDuration); - await _controller.pause(); + await controller.pause(); - expect( - await _controller.position, greaterThan(const Duration(seconds: 0))); + expect(await controller.position, greaterThan(Duration.zero)); await expectLater(started.future, completes); await expectLater(ended.future, completes); @@ -155,7 +155,7 @@ void main() { testWidgets('live stream duration != 0', (WidgetTester tester) async { final MiniController livestreamController = MiniController.network( - 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8', + 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8', ); await livestreamController.initialize(); diff --git a/packages/video_player/video_player_android/example/lib/main.dart b/packages/video_player/video_player_android/example/lib/main.dart index cab6eb802ca5..bca4e291efff 100644 --- a/packages/video_player/video_player_android/example/lib/main.dart +++ b/packages/video_player/video_player_android/example/lib/main.dart @@ -4,7 +4,6 @@ // ignore_for_file: public_member_api_docs -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'mini_controller.dart'; diff --git a/packages/video_player/video_player_android/example/lib/mini_controller.dart b/packages/video_player/video_player_android/example/lib/mini_controller.dart index 9bb8e90b65ae..24a9e0297df2 100644 --- a/packages/video_player/video_player_android/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_android/example/lib/mini_controller.dart @@ -8,7 +8,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; @@ -326,8 +325,8 @@ class MiniController extends ValueNotifier { Future seekTo(Duration position) async { if (position > value.duration) { position = value.duration; - } else if (position < const Duration()) { - position = const Duration(); + } else if (position < Duration.zero) { + position = Duration.zero; } await _platform.seekTo(_textureId, position); _updatePosition(position); @@ -352,14 +351,14 @@ class MiniController extends ValueNotifier { /// Widget that displays the video controlled by [controller]. class VideoPlayer extends StatefulWidget { /// Uses the given [controller] for all video rendered in this widget. - const VideoPlayer(this.controller); + const VideoPlayer(this.controller, {Key? key}) : super(key: key); /// The [MiniController] responsible for the video being rendered in /// this widget. final MiniController controller; @override - _VideoPlayerState createState() => _VideoPlayerState(); + State createState() => _VideoPlayerState(); } class _VideoPlayerState extends State { @@ -451,14 +450,14 @@ class _VideoScrubberState extends State<_VideoScrubber> { class VideoProgressIndicator extends StatefulWidget { /// Construct an instance that displays the play/buffering status of the video /// controlled by [controller]. - const VideoProgressIndicator(this.controller); + const VideoProgressIndicator(this.controller, {Key? key}) : super(key: key); /// The [MiniController] that actually associates a video with this /// widget. final MiniController controller; @override - _VideoProgressIndicatorState createState() => _VideoProgressIndicatorState(); + State createState() => _VideoProgressIndicatorState(); } class _VideoProgressIndicatorState extends State { @@ -522,17 +521,16 @@ class _VideoProgressIndicatorState extends State { ); } else { progressIndicator = const LinearProgressIndicator( - value: null, valueColor: AlwaysStoppedAnimation(playedColor), backgroundColor: backgroundColor, ); } return _VideoScrubber( + controller: controller, child: Padding( padding: const EdgeInsets.only(top: 5.0), child: progressIndicator, ), - controller: controller, ); } } diff --git a/packages/video_player/video_player_android/example/pubspec.yaml b/packages/video_player/video_player_android/example/pubspec.yaml index 14c06f4123d4..d935244ed924 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.8.0" + flutter: ">=2.10.0" dependencies: flutter: diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index d713b282d197..cee6d7d38f66 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -14,7 +13,7 @@ import 'messages.g.dart'; /// An Android implementation of [VideoPlayerPlatform] that uses the /// Pigeon-generated [VideoPlayerApi]. class AndroidVideoPlayer extends VideoPlayerPlatform { - final VideoPlayerApi _api = VideoPlayerApi(); + final AndroidVideoPlayerApi _api = AndroidVideoPlayerApi(); /// Registers this class as the default instance of [PathProviderPlatform]. static void registerWith() { @@ -131,6 +130,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { duration: Duration(milliseconds: map['duration'] as int), size: Size((map['width'] as num?)?.toDouble() ?? 0.0, (map['height'] as num?)?.toDouble() ?? 0.0), + rotationCorrection: map['rotationCorrection'] as int? ?? 0, ); case 'completed': return VideoEvent( diff --git a/packages/video_player/video_player_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart index 5fa09e33bc7e..0dadd2efc67e 100644 --- a/packages/video_player/video_player_android/lib/src/messages.g.dart +++ b/packages/video_player/video_player_android/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v1.0.16), do not edit directly. +// Autogenerated from Pigeon (v2.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 // @dart = 2.12 @@ -191,8 +191,8 @@ class MixWithOthersMessage { } } -class _VideoPlayerApiCodec extends StandardMessageCodec { - const _VideoPlayerApiCodec(); +class _AndroidVideoPlayerApiCodec extends StandardMessageCodec { + const _AndroidVideoPlayerApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { if (value is CreateMessage) { @@ -251,20 +251,20 @@ class _VideoPlayerApiCodec extends StandardMessageCodec { } } -class VideoPlayerApi { - /// Constructor for [VideoPlayerApi]. The [binaryMessenger] named argument is +class AndroidVideoPlayerApi { + /// Constructor for [AndroidVideoPlayerApi]. 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. - VideoPlayerApi({BinaryMessenger? binaryMessenger}) + AndroidVideoPlayerApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _VideoPlayerApiCodec(); + static const MessageCodec codec = _AndroidVideoPlayerApiCodec(); Future initialize() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.initialize', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.initialize', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel.send(null) as Map?; @@ -272,7 +272,6 @@ class VideoPlayerApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -289,15 +288,14 @@ class VideoPlayerApi { Future create(CreateMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.create', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.create', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -307,6 +305,11 @@ class VideoPlayerApi { 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 TextureMessage?)!; } @@ -314,15 +317,14 @@ class VideoPlayerApi { Future dispose(TextureMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.dispose', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.dispose', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -339,15 +341,14 @@ class VideoPlayerApi { Future setLooping(LoopingMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setLooping', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.setLooping', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -364,15 +365,14 @@ class VideoPlayerApi { Future setVolume(VolumeMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setVolume', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.setVolume', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -389,15 +389,14 @@ class VideoPlayerApi { Future setPlaybackSpeed(PlaybackSpeedMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.setPlaybackSpeed', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -414,15 +413,14 @@ class VideoPlayerApi { Future play(TextureMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.play', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.play', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -439,15 +437,14 @@ class VideoPlayerApi { Future position(TextureMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.position', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.position', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -457,6 +454,11 @@ class VideoPlayerApi { 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 PositionMessage?)!; } @@ -464,15 +466,14 @@ class VideoPlayerApi { Future seekTo(PositionMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.seekTo', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.seekTo', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -489,15 +490,14 @@ class VideoPlayerApi { Future pause(TextureMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.pause', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.pause', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -514,15 +514,14 @@ class VideoPlayerApi { Future setMixWithOthers(MixWithOthersMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.setMixWithOthers', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index 9efbfc947b5c..bf552f9369df 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -57,7 +57,7 @@ class MixWithOthersMessage { } @HostApi(dartHostTestHandler: 'TestHostVideoPlayerApi') -abstract class VideoPlayerApi { +abstract class AndroidVideoPlayerApi { void initialize(); TextureMessage create(CreateMessage msg); void dispose(TextureMessage msg); diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index c40d5185e854..1df1deeebf22 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -1,12 +1,12 @@ name: video_player_android description: Android implementation of the video_player plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_android +repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.3.0 +version: 2.3.9 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.0" flutter: plugin: @@ -20,9 +20,9 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ">=4.2.0 <6.0.0" + video_player_platform_interface: ^5.1.1 dev_dependencies: flutter_test: sdk: flutter - pigeon: ^1.0.16 + pigeon: ^2.0.1 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 b7cf763e16a6..fad9617ddad9 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 @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import import 'dart:ui'; import 'package:flutter/services.dart'; @@ -253,6 +255,20 @@ void main() { }), (ByteData? data) {}); + await _ambiguate(ServicesBinding.instance) + ?.defaultBinaryMessenger + .handlePlatformMessage( + 'flutter.io/videoPlayer/videoEvents123', + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'initialized', + 'duration': 98765, + 'width': 1920, + 'height': 1080, + 'rotationCorrection': 180, + }), + (ByteData? data) {}); + await _ambiguate(ServicesBinding.instance) ?.defaultBinaryMessenger .handlePlatformMessage( @@ -312,13 +328,20 @@ void main() { eventType: VideoEventType.initialized, duration: const Duration(milliseconds: 98765), size: const Size(1920, 1080), + rotationCorrection: 0, + ), + VideoEvent( + eventType: VideoEventType.initialized, + duration: const Duration(milliseconds: 98765), + size: const Size(1920, 1080), + rotationCorrection: 180, ), VideoEvent(eventType: VideoEventType.completed), VideoEvent( eventType: VideoEventType.bufferingUpdate, buffered: [ DurationRange( - const Duration(milliseconds: 0), + Duration.zero, const Duration(milliseconds: 1234), ), DurationRange( diff --git a/packages/video_player/video_player_android/test/test_api.dart b/packages/video_player/video_player_android/test/test_api.dart index e8bc9d85283f..6361522e247c 100644 --- a/packages/video_player/video_player_android/test/test_api.dart +++ b/packages/video_player/video_player_android/test/test_api.dart @@ -1,12 +1,15 @@ // 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 (v1.0.16), do not edit directly. +// Autogenerated from Pigeon (v2.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 +// ignore_for_file: avoid_relative_lib_imports // @dart = 2.12 import 'dart:async'; import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -92,7 +95,7 @@ abstract class TestHostVideoPlayerApi { {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.initialize', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.initialize', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); @@ -106,18 +109,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.create', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.create', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.create was null.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.create was null.'); final List args = (message as List?)!; final CreateMessage? arg_msg = (args[0] as CreateMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.create was null, expected non-null CreateMessage.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.create was null, expected non-null CreateMessage.'); final TextureMessage output = api.create(arg_msg!); return {'result': output}; }); @@ -125,18 +128,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.dispose', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.dispose', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.dispose was null.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.dispose was null.'); final List args = (message as List?)!; final TextureMessage? arg_msg = (args[0] as TextureMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.dispose was null, expected non-null TextureMessage.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.dispose was null, expected non-null TextureMessage.'); api.dispose(arg_msg!); return {}; }); @@ -144,18 +147,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setLooping', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.setLooping', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setLooping was null.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.setLooping was null.'); final List args = (message as List?)!; final LoopingMessage? arg_msg = (args[0] as LoopingMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setLooping was null, expected non-null LoopingMessage.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.setLooping was null, expected non-null LoopingMessage.'); api.setLooping(arg_msg!); return {}; }); @@ -163,18 +166,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setVolume', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.setVolume', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setVolume was null.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.setVolume was null.'); final List args = (message as List?)!; final VolumeMessage? arg_msg = (args[0] as VolumeMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setVolume was null, expected non-null VolumeMessage.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.setVolume was null, expected non-null VolumeMessage.'); api.setVolume(arg_msg!); return {}; }); @@ -182,19 +185,19 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.setPlaybackSpeed', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed was null.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.setPlaybackSpeed was null.'); final List args = (message as List?)!; final PlaybackSpeedMessage? arg_msg = (args[0] as PlaybackSpeedMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed was null, expected non-null PlaybackSpeedMessage.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.setPlaybackSpeed was null, expected non-null PlaybackSpeedMessage.'); api.setPlaybackSpeed(arg_msg!); return {}; }); @@ -202,18 +205,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.play', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.play', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.play was null.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.play was null.'); final List args = (message as List?)!; final TextureMessage? arg_msg = (args[0] as TextureMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.play was null, expected non-null TextureMessage.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.play was null, expected non-null TextureMessage.'); api.play(arg_msg!); return {}; }); @@ -221,18 +224,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.position', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.position', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.position was null.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.position was null.'); final List args = (message as List?)!; final TextureMessage? arg_msg = (args[0] as TextureMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.position was null, expected non-null TextureMessage.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.position was null, expected non-null TextureMessage.'); final PositionMessage output = api.position(arg_msg!); return {'result': output}; }); @@ -240,18 +243,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.seekTo', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.seekTo', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.seekTo was null.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.seekTo was null.'); final List args = (message as List?)!; final PositionMessage? arg_msg = (args[0] as PositionMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.seekTo was null, expected non-null PositionMessage.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.seekTo was null, expected non-null PositionMessage.'); api.seekTo(arg_msg!); return {}; }); @@ -259,18 +262,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.pause', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.pause', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.pause was null.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.pause was null.'); final List args = (message as List?)!; final TextureMessage? arg_msg = (args[0] as TextureMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.pause was null, expected non-null TextureMessage.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.pause was null, expected non-null TextureMessage.'); api.pause(arg_msg!); return {}; }); @@ -278,19 +281,19 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', codec, + 'dev.flutter.pigeon.AndroidVideoPlayerApi.setMixWithOthers', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers was null.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.setMixWithOthers was null.'); final List args = (message as List?)!; final MixWithOthersMessage? arg_msg = (args[0] as MixWithOthersMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers was null, expected non-null MixWithOthersMessage.'); + 'Argument for dev.flutter.pigeon.AndroidVideoPlayerApi.setMixWithOthers was null, expected non-null MixWithOthersMessage.'); api.setMixWithOthers(arg_msg!); return {}; }); diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index d6ecabd4b9ad..ed2f345784bd 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,41 @@ +## 2.3.7 + +* Fixes a bug where the aspect ratio of some HLS videos are incorrectly inverted. +* Updates code for `no_leading_underscores_for_local_identifiers` lint. + +## 2.3.6 + +* Fixes a bug in iOS 16 where videos from protected live streams are not shown. +* Updates minimum Flutter version to 2.10. +* Fixes violations of new analysis option use_named_constants. +* Fixes avoid_redundant_argument_values lint warnings and minor typos. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). + +## 2.3.5 + +* Updates references to the obsolete master branch. + +## 2.3.4 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 2.3.3 + +* Fix XCUITest based on the new voice over announcement for tooltips. + See: https://github.com/flutter/flutter/pull/87684 + +## 2.3.2 + +* Applies the standardized transform for videos with different orientations. + +## 2.3.1 + +* Renames internal method channels to avoid potential confusion with the + default implementation's method channel. +* Updates Pigeon to 2.0.1. + ## 2.3.0 * Updates Pigeon to ^1.0.16. diff --git a/packages/video_player/video_player_avfoundation/CONTRIBUTING.md b/packages/video_player/video_player_avfoundation/CONTRIBUTING.md index 8dfec9faf809..e06f2233278b 100644 --- a/packages/video_player/video_player_avfoundation/CONTRIBUTING.md +++ b/packages/video_player/video_player_avfoundation/CONTRIBUTING.md @@ -25,7 +25,7 @@ Then, run the commands above. When you run `pub get` it should warn you that you're using an override. If you do this, you will need to publish pigeon before you can land the updates to this package, since the CI tests run the analysis using latest published version of -pigeon, not your version or the version on master. +pigeon, not your version or the version on `main`. In either case, the configuration will be obtained automatically from the `pigeons/messages.dart` file (see `ConfigurePigeon` at the top of that file). diff --git a/packages/video_player/video_player_avfoundation/example/README.md b/packages/video_player/video_player_avfoundation/example/README.md index 8ceb0ff485fa..96b8bb17dbff 100644 --- a/packages/video_player/video_player_avfoundation/example/README.md +++ b/packages/video_player/video_player_avfoundation/example/README.md @@ -1,8 +1,9 @@ -# video_player_example +# Platform Implementation Test App -Demonstrates how to use the video_player plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart b/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart index a457d9226e3e..5027973a660d 100644 --- a/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.dart @@ -24,10 +24,13 @@ const Duration _playDuration = Duration(seconds: 1); const String _videoAssetKey = 'assets/Butterfly-209.mp4'; // Returns the URL to load an asset from this example app as a network source. +// +// TODO(stuartmorgan): Convert this to a local `HttpServer` that vends the +// assets directly, https://github.com/flutter/flutter/issues/95420 String getUrlForAssetAsNetworkSource(String assetKey) { return 'https://github.com/flutter/plugins/blob/' // This hash can be rolled forward to pick up newly-added assets. - 'cba393233e559c925a4daf71b06b4bb01c606762' + 'cb381ced070d356799dddf24aca38ce0579d3d7b' '/packages/video_player/video_player/example/' '$assetKey' '?raw=true'; @@ -36,12 +39,12 @@ String getUrlForAssetAsNetworkSource(String assetKey) { void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - late MiniController _controller; - tearDown(() async => _controller.dispose()); + late MiniController controller; + tearDown(() async => controller.dispose()); group('asset videos', () { setUp(() { - _controller = MiniController.asset(_videoAssetKey); + controller = MiniController.asset(_videoAssetKey); }); testWidgets('registers expected implementation', @@ -51,51 +54,50 @@ void main() { }); testWidgets('can be initialized', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); - expect(_controller.value.isInitialized, true); - expect(await _controller.position, const Duration(seconds: 0)); - expect(_controller.value.duration, + expect(controller.value.isInitialized, true); + expect(await controller.position, Duration.zero); + expect(controller.value.duration, const Duration(seconds: 7, milliseconds: 540)); }); testWidgets('can be played', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); - await _controller.play(); + await controller.play(); await tester.pumpAndSettle(_playDuration); - expect( - await _controller.position, greaterThan(const Duration(seconds: 0))); + expect(await controller.position, greaterThan(Duration.zero)); }); testWidgets('can seek', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); - await _controller.seekTo(const Duration(seconds: 3)); + await controller.seekTo(const Duration(seconds: 3)); // TODO(stuartmorgan): Switch to _controller.position once seekTo is // fixed on the native side to wait for completion, so this is testing // the native code rather than the MiniController position cache. - expect(_controller.value.position, const Duration(seconds: 3)); + expect(controller.value.position, const Duration(seconds: 3)); }); testWidgets('can be paused', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); // Play for a second, then pause, and then wait a second. - await _controller.play(); + await controller.play(); await tester.pumpAndSettle(_playDuration); - await _controller.pause(); - final Duration pausedPosition = (await _controller.position)!; + await controller.pause(); + final Duration pausedPosition = (await controller.position)!; await tester.pumpAndSettle(_playDuration); // Verify that we stopped playing after the pause. // TODO(stuartmorgan): Investigate why this has a slight discrepency, and // fix it if possible. Is AVPlayer's pause method internally async? const Duration allowableDelta = Duration(milliseconds: 10); - expect(await _controller.position, - lessThan(pausedPosition + allowableDelta)); + expect( + await controller.position, lessThan(pausedPosition + allowableDelta)); }); }); @@ -110,53 +112,51 @@ void main() { final File file = File('$tempDir/$filename'); await file.writeAsBytes(bytes.buffer.asInt8List()); - _controller = MiniController.file(file); + controller = MiniController.file(file); }); testWidgets('test video player using static file() method as constructor', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); - await _controller.play(); + await controller.play(); await tester.pumpAndSettle(_playDuration); - expect( - await _controller.position, greaterThan(const Duration(seconds: 0))); + expect(await controller.position, greaterThan(Duration.zero)); }); }); group('network videos', () { setUp(() { final String videoUrl = getUrlForAssetAsNetworkSource(_videoAssetKey); - _controller = MiniController.network(videoUrl); + controller = MiniController.network(videoUrl); }); testWidgets('reports buffering status', (WidgetTester tester) async { - await _controller.initialize(); + await controller.initialize(); final Completer started = Completer(); final Completer ended = Completer(); - _controller.addListener(() { - if (!started.isCompleted && _controller.value.isBuffering) { + controller.addListener(() { + if (!started.isCompleted && controller.value.isBuffering) { started.complete(); } if (started.isCompleted && - !_controller.value.isBuffering && + !controller.value.isBuffering && !ended.isCompleted) { ended.complete(); } }); - await _controller.play(); - await _controller.seekTo(const Duration(seconds: 5)); + await controller.play(); + await controller.seekTo(const Duration(seconds: 5)); await tester.pumpAndSettle(_playDuration); - await _controller.pause(); + await controller.pause(); // TODO(stuartmorgan): Switch to _controller.position once seekTo is // fixed on the native side to wait for completion, so this is testing // the native code rather than the MiniController position cache. - expect( - _controller.value.position, greaterThan(const Duration(seconds: 0))); + expect(controller.value.position, greaterThan(Duration.zero)); await expectLater(started.future, completes); await expectLater(ended.future, completes); @@ -167,7 +167,7 @@ void main() { testWidgets('live stream duration != 0', (WidgetTester tester) async { final MiniController livestreamController = MiniController.network( - 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8', + 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8', ); await livestreamController.initialize(); diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m index 6d8b3965fd94..f9f66e04bcb3 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m @@ -7,21 +7,94 @@ @import XCTest; #import +#import @interface FLTVideoPlayer : NSObject @property(readonly, nonatomic) AVPlayer *player; +@property(readonly, nonatomic) AVPlayerLayer *playerLayer; @end -@interface FLTVideoPlayerPlugin (Test) +@interface FLTVideoPlayerPlugin (Test) @property(readonly, strong, nonatomic) NSMutableDictionary *playersByTextureId; @end +@interface FakeAVAssetTrack : AVAssetTrack +@property(readonly, nonatomic) CGAffineTransform preferredTransform; +@property(readonly, nonatomic) CGSize naturalSize; +@property(readonly, nonatomic) UIImageOrientation orientation; +- (instancetype)initWithOrientation:(UIImageOrientation)orientation; +@end + +@implementation FakeAVAssetTrack + +- (instancetype)initWithOrientation:(UIImageOrientation)orientation { + _orientation = orientation; + _naturalSize = CGSizeMake(800, 600); + return self; +} + +- (CGAffineTransform)preferredTransform { + switch (_orientation) { + case UIImageOrientationUp: + return CGAffineTransformMake(1, 0, 0, 1, 0, 0); + case UIImageOrientationDown: + return CGAffineTransformMake(-1, 0, 0, -1, 0, 0); + case UIImageOrientationLeft: + return CGAffineTransformMake(0, -1, 1, 0, 0, 0); + case UIImageOrientationRight: + return CGAffineTransformMake(0, 1, -1, 0, 0, 0); + case UIImageOrientationUpMirrored: + return CGAffineTransformMake(-1, 0, 0, 1, 0, 0); + case UIImageOrientationDownMirrored: + return CGAffineTransformMake(1, 0, 0, -1, 0, 0); + case UIImageOrientationLeftMirrored: + return CGAffineTransformMake(0, -1, -1, 0, 0, 0); + case UIImageOrientationRightMirrored: + return CGAffineTransformMake(0, 1, 1, 0, 0, 0); + } +} + +@end + @interface VideoPlayerTests : XCTestCase @end @implementation VideoPlayerTests +- (void)testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream { + // This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 + // (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some + // video streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). An + // invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams + // for issue #1, and restore the correct width and height for issue #2. + NSObject *registry = + (NSObject *)[[UIApplication sharedApplication] delegate]; + NSObject *registrar = + [registry registrarForPlugin:@"testPlayerLayerWorkaround"]; + FLTVideoPlayerPlugin *videoPlayerPlugin = + [[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar]; + + FlutterError *error; + [videoPlayerPlugin initialize:&error]; + XCTAssertNil(error); + + FLTCreateMessage *create = [FLTCreateMessage + makeWithAsset:nil + uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" + packageName:nil + formatHint:nil + httpHeaders:@{}]; + FLTTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(textureMessage); + FLTVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureMessage.textureId]; + XCTAssertNotNil(player); + + XCTAssertNotNil(player.playerLayer, @"AVPlayerLayer should be present."); + XCTAssertNotNil(player.playerLayer.superlayer, @"AVPlayerLayer should be added on screen."); +} + - (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry { NSObject *mockTextureRegistry = OCMProtocolMock(@protocol(FlutterTextureRegistry)); @@ -121,6 +194,17 @@ - (void)testHLSControls { XCTAssertEqualWithAccuracy([videoInitialization[@"duration"] intValue], 4000, 200); } +- (void)testTransformFix { + [self validateTransformFixForOrientation:UIImageOrientationUp]; + [self validateTransformFixForOrientation:UIImageOrientationDown]; + [self validateTransformFixForOrientation:UIImageOrientationLeft]; + [self validateTransformFixForOrientation:UIImageOrientationRight]; + [self validateTransformFixForOrientation:UIImageOrientationUpMirrored]; + [self validateTransformFixForOrientation:UIImageOrientationDownMirrored]; + [self validateTransformFixForOrientation:UIImageOrientationLeftMirrored]; + [self validateTransformFixForOrientation:UIImageOrientationRightMirrored]; +} + - (NSDictionary *)testPlugin:(FLTVideoPlayerPlugin *)videoPlayerPlugin uri:(NSString *)uri { FlutterError *error; @@ -175,4 +259,47 @@ - (void)testHLSControls { return initializationEvent; } +- (void)validateTransformFixForOrientation:(UIImageOrientation)orientation { + AVAssetTrack *track = [[FakeAVAssetTrack alloc] initWithOrientation:orientation]; + CGAffineTransform t = FLTGetStandardizedTransformForTrack(track); + CGSize size = track.naturalSize; + CGFloat expectX, expectY; + switch (orientation) { + case UIImageOrientationUp: + expectX = 0; + expectY = 0; + break; + case UIImageOrientationDown: + expectX = size.width; + expectY = size.height; + break; + case UIImageOrientationLeft: + expectX = 0; + expectY = size.width; + break; + case UIImageOrientationRight: + expectX = size.height; + expectY = 0; + break; + case UIImageOrientationUpMirrored: + expectX = size.width; + expectY = 0; + break; + case UIImageOrientationDownMirrored: + expectX = 0; + expectY = size.height; + break; + case UIImageOrientationLeftMirrored: + expectX = size.height; + expectY = size.width; + break; + case UIImageOrientationRightMirrored: + expectX = 0; + expectY = 0; + break; + } + XCTAssertEqual(t.tx, expectX); + XCTAssertEqual(t.ty, expectY); +} + @end diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m index 2933cf36feae..54c97030c3ae 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m @@ -4,6 +4,7 @@ @import os.log; @import XCTest; +@import CoreGraphics; @interface VideoPlayerUITests : XCTestCase @property(nonatomic, strong) XCUIApplication *app; @@ -30,19 +31,23 @@ - (void)testPlayVideo { XCTAssertTrue([playButton waitForExistenceWithTimeout:30.0]); [playButton tap]; - XCUIElement *playbackSpeed1x = app.staticTexts[@"Playback speed\n1.0x"]; - XCTAssertTrue([playbackSpeed1x waitForExistenceWithTimeout:30.0]); + NSPredicate *find1xButton = [NSPredicate predicateWithFormat:@"label CONTAINS '1.0x'"]; + XCUIElement *playbackSpeed1x = [app.staticTexts elementMatchingPredicate:find1xButton]; + BOOL foundPlaybackSpeed1x = [playbackSpeed1x waitForExistenceWithTimeout:30.0]; + XCTAssertTrue(foundPlaybackSpeed1x); [playbackSpeed1x tap]; XCUIElement *playbackSpeed5xButton = app.buttons[@"5.0x"]; XCTAssertTrue([playbackSpeed5xButton waitForExistenceWithTimeout:30.0]); [playbackSpeed5xButton tap]; - XCUIElement *playbackSpeed5x = app.staticTexts[@"Playback speed\n5.0x"]; - XCTAssertTrue([playbackSpeed5x waitForExistenceWithTimeout:30.0]); + NSPredicate *find5xButton = [NSPredicate predicateWithFormat:@"label CONTAINS '5.0x'"]; + XCUIElement *playbackSpeed5x = [app.staticTexts elementMatchingPredicate:find5xButton]; + BOOL foundPlaybackSpeed5x = [playbackSpeed5x waitForExistenceWithTimeout:30.0]; + XCTAssertTrue(foundPlaybackSpeed5x); // Cycle through tabs. - for (NSString *tabName in @[ @"Asset", @"Remote" ]) { + for (NSString *tabName in @[ @"Asset mp4", @"Remote mp4" ]) { NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName]; XCUIElement *unselectedTab = [app.staticTexts elementMatchingPredicate:predicate]; XCTAssertTrue([unselectedTab waitForExistenceWithTimeout:30.0]); diff --git a/packages/video_player/video_player_avfoundation/example/lib/main.dart b/packages/video_player/video_player_avfoundation/example/lib/main.dart index cab6eb802ca5..d385fd0ee66a 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/main.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/main.dart @@ -4,7 +4,6 @@ // ignore_for_file: public_member_api_docs -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'mini_controller.dart'; @@ -21,7 +20,7 @@ class _App extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultTabController( - length: 2, + length: 3, child: Scaffold( key: const ValueKey('home_page'), appBar: AppBar( @@ -31,15 +30,20 @@ class _App extends StatelessWidget { tabs: [ Tab( icon: Icon(Icons.cloud), - text: 'Remote', + text: 'Remote mp4', ), - Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'), + Tab( + icon: Icon(Icons.favorite), + text: 'Remote enc m3u8', + ), + Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset mp4'), ], ), ), body: TabBarView( children: [ _BumbleBeeRemoteVideo(), + _BumbleBeeEncryptedLiveStream(), _ButterFlyAssetVideo(), ], ), @@ -157,6 +161,59 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { } } +class _BumbleBeeEncryptedLiveStream extends StatefulWidget { + @override + _BumbleBeeEncryptedLiveStreamState createState() => + _BumbleBeeEncryptedLiveStreamState(); +} + +class _BumbleBeeEncryptedLiveStreamState + extends State<_BumbleBeeEncryptedLiveStream> { + late MiniController _controller; + + @override + void initState() { + super.initState(); + _controller = MiniController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/encrypted_bee.m3u8', + ); + + _controller.addListener(() { + setState(() {}); + }); + _controller.initialize(); + + _controller.play(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container(padding: const EdgeInsets.only(top: 20.0)), + const Text('With remote encrypted m3u8'), + Container( + padding: const EdgeInsets.all(20), + child: _controller.value.isInitialized + ? AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ) + : const Text('loading...'), + ), + ], + ), + ); + } +} + class _ControlsOverlay extends StatelessWidget { const _ControlsOverlay({Key? key, required this.controller}) : super(key: key); diff --git a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart index 9bb8e90b65ae..24a9e0297df2 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart @@ -8,7 +8,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; @@ -326,8 +325,8 @@ class MiniController extends ValueNotifier { Future seekTo(Duration position) async { if (position > value.duration) { position = value.duration; - } else if (position < const Duration()) { - position = const Duration(); + } else if (position < Duration.zero) { + position = Duration.zero; } await _platform.seekTo(_textureId, position); _updatePosition(position); @@ -352,14 +351,14 @@ class MiniController extends ValueNotifier { /// Widget that displays the video controlled by [controller]. class VideoPlayer extends StatefulWidget { /// Uses the given [controller] for all video rendered in this widget. - const VideoPlayer(this.controller); + const VideoPlayer(this.controller, {Key? key}) : super(key: key); /// The [MiniController] responsible for the video being rendered in /// this widget. final MiniController controller; @override - _VideoPlayerState createState() => _VideoPlayerState(); + State createState() => _VideoPlayerState(); } class _VideoPlayerState extends State { @@ -451,14 +450,14 @@ class _VideoScrubberState extends State<_VideoScrubber> { class VideoProgressIndicator extends StatefulWidget { /// Construct an instance that displays the play/buffering status of the video /// controlled by [controller]. - const VideoProgressIndicator(this.controller); + const VideoProgressIndicator(this.controller, {Key? key}) : super(key: key); /// The [MiniController] that actually associates a video with this /// widget. final MiniController controller; @override - _VideoProgressIndicatorState createState() => _VideoProgressIndicatorState(); + State createState() => _VideoProgressIndicatorState(); } class _VideoProgressIndicatorState extends State { @@ -522,17 +521,16 @@ class _VideoProgressIndicatorState extends State { ); } else { progressIndicator = const LinearProgressIndicator( - value: null, valueColor: AlwaysStoppedAnimation(playedColor), backgroundColor: backgroundColor, ); } return _VideoScrubber( + controller: controller, child: Padding( padding: const EdgeInsets.only(top: 5.0), child: progressIndicator, ), - controller: controller, ); } } diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml index 40b88d577ce6..9123b15aa721 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.8.0" + flutter: ">=2.10.0" dependencies: flutter: diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/AVAssetTrackUtils.h b/packages/video_player/video_player_avfoundation/ios/Classes/AVAssetTrackUtils.h new file mode 100644 index 000000000000..9d736bc21afe --- /dev/null +++ b/packages/video_player/video_player_avfoundation/ios/Classes/AVAssetTrackUtils.h @@ -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 + +/** + * Returns a standardized transform + * according to the orientation of the track. + * + * Note: https://stackoverflow.com/questions/64161544 + * `AVAssetTrack.preferredTransform` can have wrong `tx` and `ty`. + */ +CGAffineTransform FLTGetStandardizedTransformForTrack(AVAssetTrack* track); diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/AVAssetTrackUtils.m b/packages/video_player/video_player_avfoundation/ios/Classes/AVAssetTrackUtils.m new file mode 100644 index 000000000000..de75859a94a4 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/ios/Classes/AVAssetTrackUtils.m @@ -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 + +CGAffineTransform FLTGetStandardizedTransformForTrack(AVAssetTrack *track) { + CGAffineTransform t = track.preferredTransform; + CGSize size = track.naturalSize; + // Each case of control flows corresponds to a specific + // `UIImageOrientation`, with 8 cases in total. + if (t.a == 1 && t.b == 0 && t.c == 0 && t.d == 1) { + // UIImageOrientationUp + t.tx = 0; + t.ty = 0; + } else if (t.a == -1 && t.b == 0 && t.c == 0 && t.d == -1) { + // UIImageOrientationDown + t.tx = size.width; + t.ty = size.height; + } else if (t.a == 0 && t.b == -1 && t.c == 1 && t.d == 0) { + // UIImageOrientationLeft + t.tx = 0; + t.ty = size.width; + } else if (t.a == 0 && t.b == 1 && t.c == -1 && t.d == 0) { + // UIImageOrientationRight + t.tx = size.height; + t.ty = 0; + } else if (t.a == -1 && t.b == 0 && t.c == 0 && t.d == 1) { + // UIImageOrientationUpMirrored + t.tx = size.width; + t.ty = 0; + } else if (t.a == 1 && t.b == 0 && t.c == 0 && t.d == -1) { + // UIImageOrientationDownMirrored + t.tx = 0; + t.ty = size.height; + } else if (t.a == 0 && t.b == -1 && t.c == -1 && t.d == 0) { + // UIImageOrientationLeftMirrored + t.tx = size.height; + t.ty = size.width; + } else if (t.a == 0 && t.b == 1 && t.c == 1 && t.d == 0) { + // UIImageOrientationRightMirrored + t.tx = 0; + t.ty = 0; + } + return t; +} diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m index 026b576cdb10..3b066769621c 100644 --- a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m @@ -3,8 +3,11 @@ // found in the LICENSE file. #import "FLTVideoPlayerPlugin.h" + #import #import + +#import "AVAssetTrackUtils.h" #import "messages.g.h" #if !__has_feature(objc_arc) @@ -33,6 +36,12 @@ - (void)onDisplayLink:(CADisplayLink *)link { @interface FLTVideoPlayer : NSObject @property(readonly, nonatomic) AVPlayer *player; @property(readonly, nonatomic) AVPlayerItemVideoOutput *videoOutput; +// This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 +// (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some video +// streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). +// An invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams +// for issue #1, and restore the correct width and height for issue #2. +@property(readonly, nonatomic) AVPlayerLayer *playerLayer; @property(readonly, nonatomic) CADisplayLink *displayLink; @property(nonatomic) FlutterEventChannel *eventChannel; @property(nonatomic) FlutterEventSink eventSink; @@ -129,6 +138,15 @@ NS_INLINE CGFloat radiansToDegrees(CGFloat radians) { return degrees; }; +NS_INLINE UIViewController *rootViewController() { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // TODO: (hellohuanlin) Provide a non-deprecated codepath. See + // https://github.com/flutter/flutter/issues/104117 + return UIApplication.sharedApplication.keyWindow.rootViewController; +#pragma clang diagnostic pop +} + - (AVMutableVideoComposition *)getVideoCompositionWithTransform:(CGAffineTransform)transform withAsset:(AVAsset *)asset withVideoTrack:(AVAssetTrack *)videoTrack { @@ -187,29 +205,6 @@ - (instancetype)initWithURL:(NSURL *)url return [self initWithPlayerItem:item frameUpdater:frameUpdater]; } -- (CGAffineTransform)fixTransform:(AVAssetTrack *)videoTrack { - CGAffineTransform transform = videoTrack.preferredTransform; - // TODO(@recastrodiaz): why do we need to do this? Why is the preferredTransform incorrect? - // At least 2 user videos show a black screen when in portrait mode if we directly use the - // videoTrack.preferredTransform Setting tx to the height of the video instead of 0, properly - // displays the video https://github.com/flutter/flutter/issues/17606#issuecomment-413473181 - if (transform.tx == 0 && transform.ty == 0) { - NSInteger rotationDegrees = (NSInteger)round(radiansToDegrees(atan2(transform.b, transform.a))); - NSLog(@"TX and TY are 0. Rotation: %ld. Natural width,height: %f, %f", (long)rotationDegrees, - videoTrack.naturalSize.width, videoTrack.naturalSize.height); - if (rotationDegrees == 90) { - NSLog(@"Setting transform tx"); - transform.tx = videoTrack.naturalSize.height; - transform.ty = 0; - } else if (rotationDegrees == 270) { - NSLog(@"Setting transform ty"); - transform.tx = 0; - transform.ty = videoTrack.naturalSize.width; - } - } - return transform; -} - - (instancetype)initWithPlayerItem:(AVPlayerItem *)item frameUpdater:(FLTFrameUpdater *)frameUpdater { self = [super init]; @@ -226,7 +221,7 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item if ([videoTrack statusOfValueForKey:@"preferredTransform" error:nil] == AVKeyValueStatusLoaded) { // Rotate the video by using a videoComposition and the preferredTransform - self->_preferredTransform = [self fixTransform:videoTrack]; + self->_preferredTransform = FLTGetStandardizedTransformForTrack(videoTrack); // Note: // https://developer.apple.com/documentation/avfoundation/avplayeritem/1388818-videocomposition // Video composition can only be used with file-based media and is not supported for @@ -247,6 +242,14 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item _player = [AVPlayer playerWithPlayerItem:item]; _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; + // This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 + // (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some + // video streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). An + // invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams + // for issue #1, and restore the correct width and height for issue #2. + _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; + [rootViewController().view.layer addSublayer:_playerLayer]; + [self createVideoOutputAndDisplayLink:frameUpdater]; [self addObservers:item]; @@ -478,6 +481,7 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments /// so the channel is going to die or is already dead. - (void)disposeSansEventChannel { _disposed = YES; + [_playerLayer removeFromSuperlayer]; [_displayLink invalidate]; AVPlayerItem *currentItem = self.player.currentItem; [currentItem removeObserver:self forKeyPath:@"status"]; @@ -499,7 +503,7 @@ - (void)dispose { @end -@interface FLTVideoPlayerPlugin () +@interface FLTVideoPlayerPlugin () @property(readonly, weak, nonatomic) NSObject *registry; @property(readonly, weak, nonatomic) NSObject *messenger; @property(readonly, strong, nonatomic) @@ -511,7 +515,7 @@ @implementation FLTVideoPlayerPlugin + (void)registerWithRegistrar:(NSObject *)registrar { FLTVideoPlayerPlugin *instance = [[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar]; [registrar publish:instance]; - FLTVideoPlayerApiSetup(registrar.messenger, instance); + FLTAVFoundationVideoPlayerApiSetup(registrar.messenger, instance); } - (instancetype)initWithRegistrar:(NSObject *)registrar { @@ -530,7 +534,7 @@ - (void)detachFromEngineForRegistrar:(NSObject *)registr // TODO(57151): This should be commented out when 57151's fix lands on stable. // This is the correct behavior we never did it in the past and the engine // doesn't currently support it. - // FLTVideoPlayerApiSetup(registrar.messenger, nil); + // FLTAVFoundationVideoPlayerApiSetup(registrar.messenger, nil); } - (FLTTextureMessage *)onPlayerSetup:(FLTVideoPlayer *)player diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.h b/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.h index 96d02d2f7360..130d4849f372 100644 --- a/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.h +++ b/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.h @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v1.0.17), do not edit directly. +// Autogenerated from Pigeon (v2.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @protocol FlutterBinaryMessenger; @@ -80,11 +80,12 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, strong) NSNumber *mixWithOthers; @end -/// The codec used by FLTVideoPlayerApi. -NSObject *FLTVideoPlayerApiGetCodec(void); +/// The codec used by FLTAVFoundationVideoPlayerApi. +NSObject *FLTAVFoundationVideoPlayerApiGetCodec(void); -@protocol FLTVideoPlayerApi +@protocol FLTAVFoundationVideoPlayerApi - (void)initialize:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. - (nullable FLTTextureMessage *)create:(FLTCreateMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; - (void)dispose:(FLTTextureMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; @@ -93,6 +94,7 @@ NSObject *FLTVideoPlayerApiGetCodec(void); - (void)setPlaybackSpeed:(FLTPlaybackSpeedMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; - (void)play:(FLTTextureMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. - (nullable FLTPositionMessage *)position:(FLTTextureMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; - (void)seekTo:(FLTPositionMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; @@ -101,7 +103,8 @@ NSObject *FLTVideoPlayerApiGetCodec(void); error:(FlutterError *_Nullable *_Nonnull)error; @end -extern void FLTVideoPlayerApiSetup(id binaryMessenger, - NSObject *_Nullable api); +extern void FLTAVFoundationVideoPlayerApiSetup( + id binaryMessenger, + NSObject *_Nullable api); NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.m b/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.m index c9acee44593c..d82dc386878d 100644 --- a/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.m +++ b/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.m @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v1.0.17), do not edit directly. +// Autogenerated from Pigeon (v2.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.g.h" #import @@ -28,6 +28,10 @@ static id GetNullableObject(NSDictionary *dict, id key) { id result = dict[key]; return (result == [NSNull null]) ? nil : result; } +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} @interface FLTTextureMessage () + (FLTTextureMessage *)fromMap:(NSDictionary *)dict; @@ -223,9 +227,9 @@ - (NSDictionary *)toMap { } @end -@interface FLTVideoPlayerApiCodecReader : FlutterStandardReader +@interface FLTAVFoundationVideoPlayerApiCodecReader : FlutterStandardReader @end -@implementation FLTVideoPlayerApiCodecReader +@implementation FLTAVFoundationVideoPlayerApiCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: @@ -255,9 +259,9 @@ - (nullable id)readValueOfType:(UInt8)type { } @end -@interface FLTVideoPlayerApiCodecWriter : FlutterStandardWriter +@interface FLTAVFoundationVideoPlayerApiCodecWriter : FlutterStandardWriter @end -@implementation FLTVideoPlayerApiCodecWriter +@implementation FLTAVFoundationVideoPlayerApiCodecWriter - (void)writeValue:(id)value { if ([value isKindOfClass:[FLTCreateMessage class]]) { [self writeByte:128]; @@ -286,38 +290,39 @@ - (void)writeValue:(id)value { } @end -@interface FLTVideoPlayerApiCodecReaderWriter : FlutterStandardReaderWriter +@interface FLTAVFoundationVideoPlayerApiCodecReaderWriter : FlutterStandardReaderWriter @end -@implementation FLTVideoPlayerApiCodecReaderWriter +@implementation FLTAVFoundationVideoPlayerApiCodecReaderWriter - (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[FLTVideoPlayerApiCodecWriter alloc] initWithData:data]; + return [[FLTAVFoundationVideoPlayerApiCodecWriter alloc] initWithData:data]; } - (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[FLTVideoPlayerApiCodecReader alloc] initWithData:data]; + return [[FLTAVFoundationVideoPlayerApiCodecReader alloc] initWithData:data]; } @end -NSObject *FLTVideoPlayerApiGetCodec() { +NSObject *FLTAVFoundationVideoPlayerApiGetCodec() { static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; dispatch_once(&sPred, ^{ - FLTVideoPlayerApiCodecReaderWriter *readerWriter = - [[FLTVideoPlayerApiCodecReaderWriter alloc] init]; + FLTAVFoundationVideoPlayerApiCodecReaderWriter *readerWriter = + [[FLTAVFoundationVideoPlayerApiCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; }); return sSharedObject; } -void FLTVideoPlayerApiSetup(id binaryMessenger, - NSObject *api) { +void FLTAVFoundationVideoPlayerApiSetup(id binaryMessenger, + NSObject *api) { { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.initialize" - binaryMessenger:binaryMessenger - codec:FLTVideoPlayerApiGetCodec()]; + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.initialize" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(initialize:)], - @"FLTVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", api); + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api initialize:&error]; @@ -328,16 +333,18 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, } } { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.create" - binaryMessenger:binaryMessenger - codec:FLTVideoPlayerApiGetCodec()]; + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.create" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(create:error:)], - @"FLTVideoPlayerApi api (%@) doesn't respond to @selector(create:error:)", api); + NSCAssert( + [api respondsToSelector:@selector(create:error:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(create:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - FLTCreateMessage *arg_msg = args[0]; + FLTCreateMessage *arg_msg = GetNullableObjectAtIndex(args, 0); FlutterError *error; FLTTextureMessage *output = [api create:arg_msg error:&error]; callback(wrapResult(output, error)); @@ -347,16 +354,18 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, } } { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.dispose" - binaryMessenger:binaryMessenger - codec:FLTVideoPlayerApiGetCodec()]; + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.dispose" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(dispose:error:)], - @"FLTVideoPlayerApi api (%@) doesn't respond to @selector(dispose:error:)", api); + NSCAssert( + [api respondsToSelector:@selector(dispose:error:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(dispose:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - FLTTextureMessage *arg_msg = args[0]; + FLTTextureMessage *arg_msg = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api dispose:arg_msg error:&error]; callback(wrapResult(nil, error)); @@ -366,16 +375,18 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, } } { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.setLooping" - binaryMessenger:binaryMessenger - codec:FLTVideoPlayerApiGetCodec()]; + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.setLooping" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setLooping:error:)], - @"FLTVideoPlayerApi api (%@) doesn't respond to @selector(setLooping:error:)", api); + NSCAssert( + [api respondsToSelector:@selector(setLooping:error:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(setLooping:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - FLTLoopingMessage *arg_msg = args[0]; + FLTLoopingMessage *arg_msg = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api setLooping:arg_msg error:&error]; callback(wrapResult(nil, error)); @@ -385,16 +396,18 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, } } { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.setVolume" - binaryMessenger:binaryMessenger - codec:FLTVideoPlayerApiGetCodec()]; + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.setVolume" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setVolume:error:)], - @"FLTVideoPlayerApi api (%@) doesn't respond to @selector(setVolume:error:)", api); + NSCAssert( + [api respondsToSelector:@selector(setVolume:error:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(setVolume:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - FLTVolumeMessage *arg_msg = args[0]; + FLTVolumeMessage *arg_msg = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api setVolume:arg_msg error:&error]; callback(wrapResult(nil, error)); @@ -404,17 +417,18 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, } } { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed" - binaryMessenger:binaryMessenger - codec:FLTVideoPlayerApiGetCodec()]; + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.setPlaybackSpeed" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(setPlaybackSpeed:error:)], - @"FLTVideoPlayerApi api (%@) doesn't respond to @selector(setPlaybackSpeed:error:)", + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(setPlaybackSpeed:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - FLTPlaybackSpeedMessage *arg_msg = args[0]; + FLTPlaybackSpeedMessage *arg_msg = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api setPlaybackSpeed:arg_msg error:&error]; callback(wrapResult(nil, error)); @@ -424,16 +438,17 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, } } { - FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.play" - binaryMessenger:binaryMessenger - codec:FLTVideoPlayerApiGetCodec()]; + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.play" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(play:error:)], - @"FLTVideoPlayerApi api (%@) doesn't respond to @selector(play:error:)", api); + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(play:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - FLTTextureMessage *arg_msg = args[0]; + FLTTextureMessage *arg_msg = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api play:arg_msg error:&error]; callback(wrapResult(nil, error)); @@ -443,16 +458,18 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, } } { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.position" - binaryMessenger:binaryMessenger - codec:FLTVideoPlayerApiGetCodec()]; + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.position" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(position:error:)], - @"FLTVideoPlayerApi api (%@) doesn't respond to @selector(position:error:)", api); + NSCAssert( + [api respondsToSelector:@selector(position:error:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(position:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - FLTTextureMessage *arg_msg = args[0]; + FLTTextureMessage *arg_msg = GetNullableObjectAtIndex(args, 0); FlutterError *error; FLTPositionMessage *output = [api position:arg_msg error:&error]; callback(wrapResult(output, error)); @@ -462,16 +479,18 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, } } { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.seekTo" - binaryMessenger:binaryMessenger - codec:FLTVideoPlayerApiGetCodec()]; + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.seekTo" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(seekTo:error:)], - @"FLTVideoPlayerApi api (%@) doesn't respond to @selector(seekTo:error:)", api); + NSCAssert( + [api respondsToSelector:@selector(seekTo:error:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(seekTo:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - FLTPositionMessage *arg_msg = args[0]; + FLTPositionMessage *arg_msg = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api seekTo:arg_msg error:&error]; callback(wrapResult(nil, error)); @@ -481,16 +500,18 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, } } { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.pause" - binaryMessenger:binaryMessenger - codec:FLTVideoPlayerApiGetCodec()]; + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.pause" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(pause:error:)], - @"FLTVideoPlayerApi api (%@) doesn't respond to @selector(pause:error:)", api); + NSCAssert( + [api respondsToSelector:@selector(pause:error:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(pause:error:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - FLTTextureMessage *arg_msg = args[0]; + FLTTextureMessage *arg_msg = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api pause:arg_msg error:&error]; callback(wrapResult(nil, error)); @@ -500,17 +521,18 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, } } { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers" - binaryMessenger:binaryMessenger - codec:FLTVideoPlayerApiGetCodec()]; + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.setMixWithOthers" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(setMixWithOthers:error:)], - @"FLTVideoPlayerApi api (%@) doesn't respond to @selector(setMixWithOthers:error:)", + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(setMixWithOthers:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; - FLTMixWithOthersMessage *arg_msg = args[0]; + FLTMixWithOthersMessage *arg_msg = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api setMixWithOthers:arg_msg error:&error]; callback(wrapResult(nil, error)); diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index bf1518dfa32a..b5ebedda41e1 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -14,7 +13,7 @@ import 'messages.g.dart'; /// An iOS implementation of [VideoPlayerPlatform] that uses the /// Pigeon-generated [VideoPlayerApi]. class AVFoundationVideoPlayer extends VideoPlayerPlatform { - final VideoPlayerApi _api = VideoPlayerApi(); + final AVFoundationVideoPlayerApi _api = AVFoundationVideoPlayerApi(); /// Registers this class as the default instance of [VideoPlayerPlatform]. static void registerWith() { diff --git a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart index 1a679d5915b4..a745c66322d4 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v1.0.17), do not edit directly. +// Autogenerated from Pigeon (v2.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 // @dart = 2.12 @@ -191,8 +191,8 @@ class MixWithOthersMessage { } } -class _VideoPlayerApiCodec extends StandardMessageCodec { - const _VideoPlayerApiCodec(); +class _AVFoundationVideoPlayerApiCodec extends StandardMessageCodec { + const _AVFoundationVideoPlayerApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { if (value is CreateMessage) { @@ -251,20 +251,20 @@ class _VideoPlayerApiCodec extends StandardMessageCodec { } } -class VideoPlayerApi { - /// Constructor for [VideoPlayerApi]. The [binaryMessenger] named argument is +class AVFoundationVideoPlayerApi { + /// Constructor for [AVFoundationVideoPlayerApi]. 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. - VideoPlayerApi({BinaryMessenger? binaryMessenger}) + AVFoundationVideoPlayerApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _VideoPlayerApiCodec(); + static const MessageCodec codec = _AVFoundationVideoPlayerApiCodec(); Future initialize() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.initialize', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.initialize', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel.send(null) as Map?; @@ -272,7 +272,6 @@ class VideoPlayerApi { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -289,15 +288,14 @@ class VideoPlayerApi { Future create(CreateMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.create', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.create', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -307,6 +305,11 @@ class VideoPlayerApi { 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 TextureMessage?)!; } @@ -314,15 +317,14 @@ class VideoPlayerApi { Future dispose(TextureMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.dispose', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.dispose', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -339,15 +341,14 @@ class VideoPlayerApi { Future setLooping(LoopingMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setLooping', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setLooping', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -364,15 +365,14 @@ class VideoPlayerApi { Future setVolume(VolumeMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setVolume', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setVolume', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -389,15 +389,14 @@ class VideoPlayerApi { Future setPlaybackSpeed(PlaybackSpeedMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setPlaybackSpeed', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -414,15 +413,14 @@ class VideoPlayerApi { Future play(TextureMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.play', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.play', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -439,15 +437,14 @@ class VideoPlayerApi { Future position(TextureMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.position', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.position', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -457,6 +454,11 @@ class VideoPlayerApi { 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 PositionMessage?)!; } @@ -464,15 +466,14 @@ class VideoPlayerApi { Future seekTo(PositionMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.seekTo', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.seekTo', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -489,15 +490,14 @@ class VideoPlayerApi { Future pause(TextureMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.pause', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.pause', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = @@ -514,15 +514,14 @@ class VideoPlayerApi { Future setMixWithOthers(MixWithOthersMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setMixWithOthers', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_msg]) as Map?; + await channel.send([arg_msg]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { final Map error = diff --git a/packages/video_player/video_player_avfoundation/pigeons/messages.dart b/packages/video_player/video_player_avfoundation/pigeons/messages.dart index d357caf81e3d..e6eda5960f29 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.dart @@ -58,7 +58,7 @@ class MixWithOthersMessage { } @HostApi(dartHostTestHandler: 'TestHostVideoPlayerApi') -abstract class VideoPlayerApi { +abstract class AVFoundationVideoPlayerApi { @ObjCSelector('initialize') void initialize(); @ObjCSelector('create:') diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 044d15b890b2..6da166791281 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -1,12 +1,12 @@ name: video_player_avfoundation description: iOS implementation of the video_player plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_avfoundation +repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.3.0 +version: 2.3.7 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.8.0" + flutter: ">=2.10.0" flutter: plugin: @@ -24,4 +24,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pigeon: ^1.0.17 + pigeon: ^2.0.1 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 9b6d1dfa195c..ea81d438ad75 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 @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import import 'dart:ui'; import 'package:flutter/services.dart'; @@ -318,7 +320,7 @@ void main() { eventType: VideoEventType.bufferingUpdate, buffered: [ DurationRange( - const Duration(milliseconds: 0), + Duration.zero, const Duration(milliseconds: 1234), ), DurationRange( diff --git a/packages/video_player/video_player_avfoundation/test/test_api.dart b/packages/video_player/video_player_avfoundation/test/test_api.dart index 191358e23024..c8f7bbd026a5 100644 --- a/packages/video_player/video_player_avfoundation/test/test_api.dart +++ b/packages/video_player/video_player_avfoundation/test/test_api.dart @@ -1,13 +1,15 @@ // 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 (v1.0.17), do not edit directly. +// Autogenerated from Pigeon (v2.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 // ignore_for_file: avoid_relative_lib_imports // @dart = 2.12 import 'dart:async'; import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -93,7 +95,7 @@ abstract class TestHostVideoPlayerApi { {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.initialize', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.initialize', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); @@ -107,18 +109,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.create', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.create', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.create was null.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.create was null.'); final List args = (message as List?)!; final CreateMessage? arg_msg = (args[0] as CreateMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.create was null, expected non-null CreateMessage.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.create was null, expected non-null CreateMessage.'); final TextureMessage output = api.create(arg_msg!); return {'result': output}; }); @@ -126,18 +128,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.dispose', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.dispose', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.dispose was null.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.dispose was null.'); final List args = (message as List?)!; final TextureMessage? arg_msg = (args[0] as TextureMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.dispose was null, expected non-null TextureMessage.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.dispose was null, expected non-null TextureMessage.'); api.dispose(arg_msg!); return {}; }); @@ -145,18 +147,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setLooping', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setLooping', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setLooping was null.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.setLooping was null.'); final List args = (message as List?)!; final LoopingMessage? arg_msg = (args[0] as LoopingMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setLooping was null, expected non-null LoopingMessage.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.setLooping was null, expected non-null LoopingMessage.'); api.setLooping(arg_msg!); return {}; }); @@ -164,18 +166,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setVolume', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setVolume', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setVolume was null.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.setVolume was null.'); final List args = (message as List?)!; final VolumeMessage? arg_msg = (args[0] as VolumeMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setVolume was null, expected non-null VolumeMessage.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.setVolume was null, expected non-null VolumeMessage.'); api.setVolume(arg_msg!); return {}; }); @@ -183,19 +185,20 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setPlaybackSpeed', + codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed was null.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.setPlaybackSpeed was null.'); final List args = (message as List?)!; final PlaybackSpeedMessage? arg_msg = (args[0] as PlaybackSpeedMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed was null, expected non-null PlaybackSpeedMessage.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.setPlaybackSpeed was null, expected non-null PlaybackSpeedMessage.'); api.setPlaybackSpeed(arg_msg!); return {}; }); @@ -203,18 +206,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.play', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.play', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.play was null.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.play was null.'); final List args = (message as List?)!; final TextureMessage? arg_msg = (args[0] as TextureMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.play was null, expected non-null TextureMessage.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.play was null, expected non-null TextureMessage.'); api.play(arg_msg!); return {}; }); @@ -222,18 +225,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.position', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.position', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.position was null.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.position was null.'); final List args = (message as List?)!; final TextureMessage? arg_msg = (args[0] as TextureMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.position was null, expected non-null TextureMessage.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.position was null, expected non-null TextureMessage.'); final PositionMessage output = api.position(arg_msg!); return {'result': output}; }); @@ -241,18 +244,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.seekTo', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.seekTo', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.seekTo was null.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.seekTo was null.'); final List args = (message as List?)!; final PositionMessage? arg_msg = (args[0] as PositionMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.seekTo was null, expected non-null PositionMessage.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.seekTo was null, expected non-null PositionMessage.'); api.seekTo(arg_msg!); return {}; }); @@ -260,18 +263,18 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.pause', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.pause', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.pause was null.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.pause was null.'); final List args = (message as List?)!; final TextureMessage? arg_msg = (args[0] as TextureMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.pause was null, expected non-null TextureMessage.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.pause was null, expected non-null TextureMessage.'); api.pause(arg_msg!); return {}; }); @@ -279,19 +282,20 @@ abstract class TestHostVideoPlayerApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', codec, + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setMixWithOthers', + codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers was null.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.setMixWithOthers was null.'); final List args = (message as List?)!; final MixWithOthersMessage? arg_msg = (args[0] as MixWithOthersMessage?); assert(arg_msg != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers was null, expected non-null MixWithOthersMessage.'); + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.setMixWithOthers was null, expected non-null MixWithOthersMessage.'); api.setMixWithOthers(arg_msg!); return {}; }); diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 52612207d8f3..05cb63835da4 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,31 @@ +## 6.0.0 + +* **BREAKING CHANGE**: Removes `MethodChannelVideoPlayer`. The default + implementation is now only a placeholder with no functionality; + implementations of `video_player` must include their own `VideoPlayerPlatform` + Dart implementation. +* Updates minimum Flutter version to 2.10. +* Fixes violations of new analysis option use_named_constants. + +## 5.1.4 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). + +## 5.1.3 + +* Updates references to the obsolete master branch. +* Removes unnecessary imports. + +## 5.1.2 + +* Adopts `Object.hash`. +* Removes obsolete dependency on `pedantic`. + +## 5.1.1 + +* Adds `rotationCorrection` (for Android playing videos recorded in landscapeRight [#60327](https://github.com/flutter/flutter/issues/60327)). + ## 5.1.0 * Adds `allowBackgroundPlayback` to `VideoPlayerOptions`. diff --git a/packages/video_player/video_player_platform_interface/CONTRIBUTING.md b/packages/video_player/video_player_platform_interface/CONTRIBUTING.md deleted file mode 100644 index dbbfbf66c9a1..000000000000 --- a/packages/video_player/video_player_platform_interface/CONTRIBUTING.md +++ /dev/null @@ -1,46 +0,0 @@ -## Updating pigeon-generated files - -**WARNING**: Because `messages.dart` is part of the public API of this package, -breaking changes in that file are breaking changes for the package. This means -that: -- You should never update the version of Pigeon used for this package unless - making a breaking change to the package for other reasons. -- Because the method channel is a legacy implementation for compatibility with - existing third-party `video_player` implementations, in many cases the best - option may be to simply not implemented new features in - `MethodChannelVideoPlayer`. Breaking changes in this package should never - be made solely to change `MethodChannelVideoPlayer`. - -### Update process - -If you update files in the pigeons/ directory, run the following -command in this directory (ignore the errors you get about -dependencies in the examples directory): - -```bash -flutter pub upgrade -flutter pub run pigeon --dart_null_safety --input pigeons/messages.dart -# git commit your changes so that your working environment is clean -(cd ../../../; ./script/tool_runner.sh format --clang-format=clang-format-7) -``` - -If you update pigeon itself and want to test the changes here, -temporarily update the pubspec.yaml by adding the following to the -`dependency_overrides` section, assuming you have checked out the -`flutter/packages` repo in a sibling directory to the `plugins` repo: - -```yaml - pigeon: - path: - ../../../../packages/packages/pigeon/ -``` - -Then, run the commands above. When you run `pub get` it should warn -you that you're using an override. If you do this, you will need to -publish pigeon before you can land the updates to this package, since -the CI tests run the analysis using latest published version of -pigeon, not your version or the version on master. - -In either case, the configuration will be obtained automatically from -the `pigeons/messages.dart` file (see `configurePigeon` at the bottom -of that file). diff --git a/packages/video_player/video_player_platform_interface/lib/messages.dart b/packages/video_player/video_player_platform_interface/lib/messages.dart deleted file mode 100644 index 831f4e3755d9..000000000000 --- a/packages/video_player/video_player_platform_interface/lib/messages.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. - -// Autogenerated from Pigeon (v0.1.21), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, cast_nullable_to_non_nullable -// @dart = 2.12 -import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; - -import 'package:flutter/services.dart'; - -class TextureMessage { - int? textureId; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['textureId'] = textureId; - return pigeonMap; - } - - static TextureMessage decode(Object message) { - final Map pigeonMap = message as Map; - return TextureMessage()..textureId = pigeonMap['textureId'] as int?; - } -} - -class CreateMessage { - String? asset; - String? uri; - String? packageName; - String? formatHint; - Map? httpHeaders; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['asset'] = asset; - pigeonMap['uri'] = uri; - pigeonMap['packageName'] = packageName; - pigeonMap['formatHint'] = formatHint; - pigeonMap['httpHeaders'] = httpHeaders; - return pigeonMap; - } - - static CreateMessage decode(Object message) { - final Map pigeonMap = message as Map; - return CreateMessage() - ..asset = pigeonMap['asset'] as String? - ..uri = pigeonMap['uri'] as String? - ..packageName = pigeonMap['packageName'] as String? - ..formatHint = pigeonMap['formatHint'] as String? - ..httpHeaders = pigeonMap['httpHeaders'] as Map?; - } -} - -class LoopingMessage { - int? textureId; - bool? isLooping; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['textureId'] = textureId; - pigeonMap['isLooping'] = isLooping; - return pigeonMap; - } - - static LoopingMessage decode(Object message) { - final Map pigeonMap = message as Map; - return LoopingMessage() - ..textureId = pigeonMap['textureId'] as int? - ..isLooping = pigeonMap['isLooping'] as bool?; - } -} - -class VolumeMessage { - int? textureId; - double? volume; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['textureId'] = textureId; - pigeonMap['volume'] = volume; - return pigeonMap; - } - - static VolumeMessage decode(Object message) { - final Map pigeonMap = message as Map; - return VolumeMessage() - ..textureId = pigeonMap['textureId'] as int? - ..volume = pigeonMap['volume'] as double?; - } -} - -class PlaybackSpeedMessage { - int? textureId; - double? speed; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['textureId'] = textureId; - pigeonMap['speed'] = speed; - return pigeonMap; - } - - static PlaybackSpeedMessage decode(Object message) { - final Map pigeonMap = message as Map; - return PlaybackSpeedMessage() - ..textureId = pigeonMap['textureId'] as int? - ..speed = pigeonMap['speed'] as double?; - } -} - -class PositionMessage { - int? textureId; - int? position; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['textureId'] = textureId; - pigeonMap['position'] = position; - return pigeonMap; - } - - static PositionMessage decode(Object message) { - final Map pigeonMap = message as Map; - return PositionMessage() - ..textureId = pigeonMap['textureId'] as int? - ..position = pigeonMap['position'] as int?; - } -} - -class MixWithOthersMessage { - bool? mixWithOthers; - - Object encode() { - final Map pigeonMap = {}; - pigeonMap['mixWithOthers'] = mixWithOthers; - return pigeonMap; - } - - static MixWithOthersMessage decode(Object message) { - final Map pigeonMap = message as Map; - return MixWithOthersMessage() - ..mixWithOthers = pigeonMap['mixWithOthers'] as bool?; - } -} - -class VideoPlayerApi { - Future initialize() async { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.initialize', StandardMessageCodec()); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } 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 { - // noop - } - } - - Future create(CreateMessage arg) async { - final Object encoded = arg.encode(); - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.create', StandardMessageCodec()); - final Map? replyMap = - await channel.send(encoded) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } 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 TextureMessage.decode(replyMap['result']!); - } - } - - Future dispose(TextureMessage arg) async { - final Object encoded = arg.encode(); - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.dispose', StandardMessageCodec()); - final Map? replyMap = - await channel.send(encoded) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } 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 { - // noop - } - } - - Future setLooping(LoopingMessage arg) async { - final Object encoded = arg.encode(); - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setLooping', StandardMessageCodec()); - final Map? replyMap = - await channel.send(encoded) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } 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 { - // noop - } - } - - Future setVolume(VolumeMessage arg) async { - final Object encoded = arg.encode(); - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setVolume', StandardMessageCodec()); - final Map? replyMap = - await channel.send(encoded) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } 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 { - // noop - } - } - - Future setPlaybackSpeed(PlaybackSpeedMessage arg) async { - final Object encoded = arg.encode(); - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', - StandardMessageCodec()); - final Map? replyMap = - await channel.send(encoded) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } 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 { - // noop - } - } - - Future play(TextureMessage arg) async { - final Object encoded = arg.encode(); - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.play', StandardMessageCodec()); - final Map? replyMap = - await channel.send(encoded) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } 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 { - // noop - } - } - - Future position(TextureMessage arg) async { - final Object encoded = arg.encode(); - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); - final Map? replyMap = - await channel.send(encoded) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } 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 PositionMessage.decode(replyMap['result']!); - } - } - - Future seekTo(PositionMessage arg) async { - final Object encoded = arg.encode(); - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.seekTo', StandardMessageCodec()); - final Map? replyMap = - await channel.send(encoded) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } 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 { - // noop - } - } - - Future pause(TextureMessage arg) async { - final Object encoded = arg.encode(); - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.pause', StandardMessageCodec()); - final Map? replyMap = - await channel.send(encoded) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } 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 { - // noop - } - } - - Future setMixWithOthers(MixWithOthersMessage arg) async { - final Object encoded = arg.encode(); - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', - StandardMessageCodec()); - final Map? replyMap = - await channel.send(encoded) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } 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 { - // noop - } - } -} diff --git a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart deleted file mode 100644 index 2aa7fb30e5f2..000000000000 --- a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart +++ /dev/null @@ -1,171 +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:ui'; - -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import 'messages.dart'; -import 'video_player_platform_interface.dart'; - -/// An implementation of [VideoPlayerPlatform] that uses method channels. -/// -/// This is the default implementation, for compatibility with existing -/// third-party implementations. It is not used by other implementations in -/// this repository. -class MethodChannelVideoPlayer extends VideoPlayerPlatform { - final VideoPlayerApi _api = VideoPlayerApi(); - - @override - Future init() { - return _api.initialize(); - } - - @override - Future dispose(int textureId) { - return _api.dispose(TextureMessage()..textureId = textureId); - } - - @override - Future create(DataSource dataSource) async { - final CreateMessage message = CreateMessage(); - - switch (dataSource.sourceType) { - case DataSourceType.asset: - message.asset = dataSource.asset; - message.packageName = dataSource.package; - break; - case DataSourceType.network: - message.uri = dataSource.uri; - message.formatHint = _videoFormatStringMap[dataSource.formatHint]; - message.httpHeaders = dataSource.httpHeaders; - break; - case DataSourceType.file: - message.uri = dataSource.uri; - break; - case DataSourceType.contentUri: - message.uri = dataSource.uri; - break; - } - - final TextureMessage response = await _api.create(message); - return response.textureId; - } - - @override - Future setLooping(int textureId, bool looping) { - return _api.setLooping(LoopingMessage() - ..textureId = textureId - ..isLooping = looping); - } - - @override - Future play(int textureId) { - return _api.play(TextureMessage()..textureId = textureId); - } - - @override - Future pause(int textureId) { - return _api.pause(TextureMessage()..textureId = textureId); - } - - @override - Future setVolume(int textureId, double volume) { - return _api.setVolume(VolumeMessage() - ..textureId = textureId - ..volume = volume); - } - - @override - Future setPlaybackSpeed(int textureId, double speed) { - assert(speed > 0); - - return _api.setPlaybackSpeed(PlaybackSpeedMessage() - ..textureId = textureId - ..speed = speed); - } - - @override - Future seekTo(int textureId, Duration position) { - return _api.seekTo(PositionMessage() - ..textureId = textureId - ..position = position.inMilliseconds); - } - - @override - Future getPosition(int textureId) async { - final PositionMessage response = - await _api.position(TextureMessage()..textureId = textureId); - return Duration(milliseconds: response.position!); - } - - @override - Stream videoEventsFor(int textureId) { - return _eventChannelFor(textureId) - .receiveBroadcastStream() - .map((dynamic event) { - final Map map = event as Map; - switch (map['event']) { - case 'initialized': - return VideoEvent( - eventType: VideoEventType.initialized, - duration: Duration(milliseconds: map['duration']! as int), - size: Size((map['width'] as num?)?.toDouble() ?? 0.0, - (map['height'] as num?)?.toDouble() ?? 0.0), - ); - case 'completed': - return VideoEvent( - eventType: VideoEventType.completed, - ); - case 'bufferingUpdate': - final List values = map['values']! as List; - - return VideoEvent( - buffered: values.map(_toDurationRange).toList(), - eventType: VideoEventType.bufferingUpdate, - ); - case 'bufferingStart': - return VideoEvent(eventType: VideoEventType.bufferingStart); - case 'bufferingEnd': - return VideoEvent(eventType: VideoEventType.bufferingEnd); - default: - return VideoEvent(eventType: VideoEventType.unknown); - } - }); - } - - @override - Widget buildView(int textureId) { - return Texture(textureId: textureId); - } - - @override - Future setMixWithOthers(bool mixWithOthers) { - return _api.setMixWithOthers( - MixWithOthersMessage()..mixWithOthers = mixWithOthers, - ); - } - - EventChannel _eventChannelFor(int textureId) { - return EventChannel('flutter.io/videoPlayer/videoEvents$textureId'); - } - - static const Map _videoFormatStringMap = - { - VideoFormat.ss: 'ss', - VideoFormat.hls: 'hls', - VideoFormat.dash: 'dash', - VideoFormat.other: 'other', - }; - - DurationRange _toDurationRange(dynamic value) { - final List pair = value as List; - return DurationRange( - Duration(milliseconds: pair[0]! as int), - Duration(milliseconds: pair[1]! as int), - ); - } -} diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 8a61005c429e..92099eb6635a 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -6,8 +6,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'method_channel_video_player.dart'; - /// The interface that implementations of video_player must implement. /// /// Platform implementations should extend this class rather than implement it as `video_player` @@ -21,11 +19,12 @@ abstract class VideoPlayerPlatform extends PlatformInterface { static final Object _token = Object(); - static VideoPlayerPlatform _instance = MethodChannelVideoPlayer(); + static VideoPlayerPlatform _instance = _PlaceholderImplementation(); - /// The default instance of [VideoPlayerPlatform] to use. + /// The instance of [VideoPlayerPlatform] to use. /// - /// Defaults to [MethodChannelVideoPlayer]. + /// Defaults to a placeholder that does not override any methods, and thus + /// throws `UnimplementedError` in most cases. static VideoPlayerPlatform get instance => _instance; /// Platform-specific plugins should override this with their own @@ -105,6 +104,8 @@ abstract class VideoPlayerPlatform extends PlatformInterface { } } +class _PlaceholderImplementation extends VideoPlayerPlatform {} + /// Description of the data source used to create an instance of /// the video player. class DataSource { @@ -199,8 +200,8 @@ class VideoEvent { /// /// The [eventType] argument is required. /// - /// Depending on the [eventType], the [duration], [size] and [buffered] - /// arguments can be null. + /// Depending on the [eventType], the [duration], [size], + /// [rotationCorrection], and [buffered] arguments can be null. // TODO(stuartmorgan): Temporarily suppress warnings about not using const // in all of the other video player packages, fix this, and then update // the other packages to use const. @@ -209,6 +210,7 @@ class VideoEvent { required this.eventType, this.duration, this.size, + this.rotationCorrection, this.buffered, }); @@ -225,6 +227,11 @@ class VideoEvent { /// Only used if [eventType] is [VideoEventType.initialized]. final Size? size; + /// Degrees to rotate the video (clockwise) so it is displayed correctly. + /// + /// Only used if [eventType] is [VideoEventType.initialized]. + final int? rotationCorrection; + /// Buffered parts of the video. /// /// Only used if [eventType] is [VideoEventType.bufferingUpdate]. @@ -238,15 +245,18 @@ class VideoEvent { eventType == other.eventType && duration == other.duration && size == other.size && + rotationCorrection == other.rotationCorrection && listEquals(buffered, other.buffered); } @override - int get hashCode => - eventType.hashCode ^ - duration.hashCode ^ - size.hashCode ^ - buffered.hashCode; + int get hashCode => Object.hash( + eventType, + duration, + size, + rotationCorrection, + buffered, + ); } /// Type of the event. @@ -335,7 +345,7 @@ class DurationRange { end == other.end; @override - int get hashCode => start.hashCode ^ end.hashCode; + int get hashCode => Object.hash(start, end); } /// [VideoPlayerOptions] can be optionally used to set additional player settings diff --git a/packages/video_player/video_player_platform_interface/pigeons/messages.dart b/packages/video_player/video_player_platform_interface/pigeons/messages.dart deleted file mode 100644 index 144edb6133b0..000000000000 --- a/packages/video_player/video_player_platform_interface/pigeons/messages.dart +++ /dev/null @@ -1,63 +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. - -// @dart = 2.9 - -import 'package:pigeon/pigeon_lib.dart'; - -class TextureMessage { - int textureId; -} - -class LoopingMessage { - int textureId; - bool isLooping; -} - -class VolumeMessage { - int textureId; - double volume; -} - -class PlaybackSpeedMessage { - int textureId; - double speed; -} - -class PositionMessage { - int textureId; - int position; -} - -class CreateMessage { - String asset; - String uri; - String packageName; - String formatHint; - Map httpHeaders; -} - -class MixWithOthersMessage { - bool mixWithOthers; -} - -@HostApi(dartHostTestHandler: 'TestHostVideoPlayerApi') -abstract class VideoPlayerApi { - void initialize(); - TextureMessage create(CreateMessage msg); - void dispose(TextureMessage msg); - void setLooping(LoopingMessage msg); - void setVolume(VolumeMessage msg); - void setPlaybackSpeed(PlaybackSpeedMessage msg); - void play(TextureMessage msg); - PositionMessage position(TextureMessage msg); - void seekTo(PositionMessage msg); - void pause(TextureMessage msg); - void setMixWithOthers(MixWithOthersMessage msg); -} - -void configurePigeon(PigeonOptions opts) { - opts.dartOut = 'lib/messages.dart'; - opts.dartTestOut = 'test/test.dart'; -} diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index b66fa0b46d75..56e132dbb3a7 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/video_player/v issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%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: 5.1.0 +version: 6.0.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -18,5 +18,3 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 - pigeon: 0.1.21 diff --git a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart deleted file mode 100644 index 75baba45f763..000000000000 --- a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart +++ /dev/null @@ -1,345 +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:ui'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:video_player_platform_interface/messages.dart'; -import 'package:video_player_platform_interface/method_channel_video_player.dart'; -import 'package:video_player_platform_interface/video_player_platform_interface.dart'; - -import 'test.dart'; - -class _ApiLogger implements TestHostVideoPlayerApi { - final List log = []; - TextureMessage? textureMessage; - CreateMessage? createMessage; - PositionMessage? positionMessage; - LoopingMessage? loopingMessage; - VolumeMessage? volumeMessage; - PlaybackSpeedMessage? playbackSpeedMessage; - MixWithOthersMessage? mixWithOthersMessage; - - @override - TextureMessage create(CreateMessage arg) { - log.add('create'); - createMessage = arg; - return TextureMessage()..textureId = 3; - } - - @override - void dispose(TextureMessage arg) { - log.add('dispose'); - textureMessage = arg; - } - - @override - void initialize() { - log.add('init'); - } - - @override - void pause(TextureMessage arg) { - log.add('pause'); - textureMessage = arg; - } - - @override - void play(TextureMessage arg) { - log.add('play'); - textureMessage = arg; - } - - @override - void setMixWithOthers(MixWithOthersMessage arg) { - log.add('setMixWithOthers'); - mixWithOthersMessage = arg; - } - - @override - PositionMessage position(TextureMessage arg) { - log.add('position'); - textureMessage = arg; - return PositionMessage()..position = 234; - } - - @override - void seekTo(PositionMessage arg) { - log.add('seekTo'); - positionMessage = arg; - } - - @override - void setLooping(LoopingMessage arg) { - log.add('setLooping'); - loopingMessage = arg; - } - - @override - void setVolume(VolumeMessage arg) { - log.add('setVolume'); - volumeMessage = arg; - } - - @override - void setPlaybackSpeed(PlaybackSpeedMessage arg) { - log.add('setPlaybackSpeed'); - playbackSpeedMessage = arg; - } -} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - // Store the initial instance before any tests change it. - final VideoPlayerPlatform initialInstance = VideoPlayerPlatform.instance; - - group('$VideoPlayerPlatform', () { - test('$MethodChannelVideoPlayer() is the default instance', () { - expect(initialInstance, isInstanceOf()); - }); - }); - - group('$MethodChannelVideoPlayer', () { - final MethodChannelVideoPlayer player = MethodChannelVideoPlayer(); - late _ApiLogger log; - - setUp(() { - log = _ApiLogger(); - TestHostVideoPlayerApi.setup(log); - }); - - test('init', () async { - await player.init(); - expect( - log.log.last, - 'init', - ); - }); - - test('dispose', () async { - await player.dispose(1); - expect(log.log.last, 'dispose'); - expect(log.textureMessage?.textureId, 1); - }); - - test('create with asset', () async { - final int? textureId = await player.create(DataSource( - sourceType: DataSourceType.asset, - asset: 'someAsset', - package: 'somePackage', - )); - expect(log.log.last, 'create'); - expect(log.createMessage?.asset, 'someAsset'); - expect(log.createMessage?.packageName, 'somePackage'); - expect(textureId, 3); - }); - - test('create with network', () async { - final int? textureId = await player.create(DataSource( - sourceType: DataSourceType.network, - uri: 'someUri', - formatHint: VideoFormat.dash, - )); - expect(log.log.last, 'create'); - expect(log.createMessage?.asset, null); - expect(log.createMessage?.uri, 'someUri'); - expect(log.createMessage?.packageName, null); - expect(log.createMessage?.formatHint, 'dash'); - expect(log.createMessage?.httpHeaders, {}); - expect(textureId, 3); - }); - - test('create with network (some headers)', () async { - final int? textureId = await player.create(DataSource( - sourceType: DataSourceType.network, - uri: 'someUri', - httpHeaders: {'Authorization': 'Bearer token'}, - )); - expect(log.log.last, 'create'); - expect(log.createMessage?.asset, null); - expect(log.createMessage?.uri, 'someUri'); - expect(log.createMessage?.packageName, null); - expect(log.createMessage?.formatHint, null); - expect(log.createMessage?.httpHeaders, - {'Authorization': 'Bearer token'}); - expect(textureId, 3); - }); - - test('create with file', () async { - final int? textureId = await player.create(DataSource( - sourceType: DataSourceType.file, - uri: 'someUri', - )); - expect(log.log.last, 'create'); - expect(log.createMessage?.uri, 'someUri'); - expect(textureId, 3); - }); - - test('setLooping', () async { - await player.setLooping(1, true); - expect(log.log.last, 'setLooping'); - expect(log.loopingMessage?.textureId, 1); - expect(log.loopingMessage?.isLooping, true); - }); - - test('play', () async { - await player.play(1); - expect(log.log.last, 'play'); - expect(log.textureMessage?.textureId, 1); - }); - - test('pause', () async { - await player.pause(1); - expect(log.log.last, 'pause'); - expect(log.textureMessage?.textureId, 1); - }); - - test('setMixWithOthers', () async { - await player.setMixWithOthers(true); - expect(log.log.last, 'setMixWithOthers'); - expect(log.mixWithOthersMessage?.mixWithOthers, true); - - await player.setMixWithOthers(false); - expect(log.log.last, 'setMixWithOthers'); - expect(log.mixWithOthersMessage?.mixWithOthers, false); - }); - - test('setVolume', () async { - await player.setVolume(1, 0.7); - expect(log.log.last, 'setVolume'); - expect(log.volumeMessage?.textureId, 1); - expect(log.volumeMessage?.volume, 0.7); - }); - - test('setPlaybackSpeed', () async { - await player.setPlaybackSpeed(1, 1.5); - expect(log.log.last, 'setPlaybackSpeed'); - expect(log.playbackSpeedMessage?.textureId, 1); - expect(log.playbackSpeedMessage?.speed, 1.5); - }); - - test('seekTo', () async { - await player.seekTo(1, const Duration(milliseconds: 12345)); - expect(log.log.last, 'seekTo'); - expect(log.positionMessage?.textureId, 1); - expect(log.positionMessage?.position, 12345); - }); - - test('getPosition', () async { - final Duration position = await player.getPosition(1); - expect(log.log.last, 'position'); - expect(log.textureMessage?.textureId, 1); - expect(position, const Duration(milliseconds: 234)); - }); - - test('videoEventsFor', () async { - _ambiguate(ServicesBinding.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 - .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'initialized', - 'duration': 98765, - 'width': 1920, - 'height': 1080, - }), - (ByteData? data) {}); - - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger - .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'completed', - }), - (ByteData? data) {}); - - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger - .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'bufferingUpdate', - 'values': >[ - [0, 1234], - [1235, 4000], - ], - }), - (ByteData? data) {}); - - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger - .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'bufferingStart', - }), - (ByteData? data) {}); - - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger - .handlePlatformMessage( - 'flutter.io/videoPlayer/videoEvents123', - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'bufferingEnd', - }), - (ByteData? data) {}); - - return const StandardMethodCodec().encodeSuccessEnvelope(null); - } else if (methodCall.method == 'cancel') { - return const StandardMethodCodec().encodeSuccessEnvelope(null); - } else { - fail('Expected listen or cancel'); - } - }, - ); - expect( - player.videoEventsFor(123), - emitsInOrder([ - VideoEvent( - eventType: VideoEventType.initialized, - duration: const Duration(milliseconds: 98765), - size: const Size(1920, 1080), - ), - VideoEvent(eventType: VideoEventType.completed), - VideoEvent( - eventType: VideoEventType.bufferingUpdate, - buffered: [ - DurationRange( - const Duration(milliseconds: 0), - const Duration(milliseconds: 1234), - ), - DurationRange( - const Duration(milliseconds: 1235), - const Duration(milliseconds: 4000), - ), - ]), - VideoEvent(eventType: VideoEventType.bufferingStart), - VideoEvent(eventType: VideoEventType.bufferingEnd), - ])); - }); - }); -} - -/// 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. -// TODO(ianh): Remove this once we roll stable in late 2021. -T? _ambiguate(T? value) => value; diff --git a/packages/video_player/video_player_platform_interface/test/test.dart b/packages/video_player/video_player_platform_interface/test/test.dart deleted file mode 100644 index a12ae45e59db..000000000000 --- a/packages/video_player/video_player_platform_interface/test/test.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. - -// Autogenerated from Pigeon (v0.1.21), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import -// @dart = 2.12 -import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:video_player_platform_interface/messages.dart'; - -abstract class TestHostVideoPlayerApi { - void initialize(); - TextureMessage create(CreateMessage arg); - void dispose(TextureMessage arg); - void setLooping(LoopingMessage arg); - void setVolume(VolumeMessage arg); - void setPlaybackSpeed(PlaybackSpeedMessage arg); - void play(TextureMessage arg); - PositionMessage position(TextureMessage arg); - void seekTo(PositionMessage arg); - void pause(TextureMessage arg); - void setMixWithOthers(MixWithOthersMessage arg); - static void setup(TestHostVideoPlayerApi? api) { - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.initialize', - StandardMessageCodec()); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - // ignore message - api.initialize(); - return {}; - }); - } - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.create', StandardMessageCodec()); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.create was null. Expected CreateMessage.'); - final CreateMessage input = CreateMessage.decode(message!); - final TextureMessage output = api.create(input); - return {'result': output.encode()}; - }); - } - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.dispose', StandardMessageCodec()); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.dispose was null. Expected TextureMessage.'); - final TextureMessage input = TextureMessage.decode(message!); - api.dispose(input); - return {}; - }); - } - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setLooping', - StandardMessageCodec()); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setLooping was null. Expected LoopingMessage.'); - final LoopingMessage input = LoopingMessage.decode(message!); - api.setLooping(input); - return {}; - }); - } - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setVolume', - StandardMessageCodec()); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setVolume was null. Expected VolumeMessage.'); - final VolumeMessage input = VolumeMessage.decode(message!); - api.setVolume(input); - return {}; - }); - } - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', - StandardMessageCodec()); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed was null. Expected PlaybackSpeedMessage.'); - final PlaybackSpeedMessage input = - PlaybackSpeedMessage.decode(message!); - api.setPlaybackSpeed(input); - return {}; - }); - } - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.play', StandardMessageCodec()); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.play was null. Expected TextureMessage.'); - final TextureMessage input = TextureMessage.decode(message!); - api.play(input); - return {}; - }); - } - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.position was null. Expected TextureMessage.'); - final TextureMessage input = TextureMessage.decode(message!); - final PositionMessage output = api.position(input); - return {'result': output.encode()}; - }); - } - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.seekTo', StandardMessageCodec()); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.seekTo was null. Expected PositionMessage.'); - final PositionMessage input = PositionMessage.decode(message!); - api.seekTo(input); - return {}; - }); - } - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.pause', StandardMessageCodec()); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.pause was null. Expected TextureMessage.'); - final TextureMessage input = TextureMessage.decode(message!); - api.pause(input); - return {}; - }); - } - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', - StandardMessageCodec()); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers was null. Expected MixWithOthersMessage.'); - final MixWithOthersMessage input = - MixWithOthersMessage.decode(message!); - api.setMixWithOthers(input); - return {}; - }); - } - } - } -} diff --git a/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart b/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart new file mode 100644 index 000000000000..8aa7ad9bd3c1 --- /dev/null +++ b/packages/video_player/video_player_platform_interface/test/video_player_platform_interface_test.dart @@ -0,0 +1,15 @@ +// 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:video_player_platform_interface/video_player_platform_interface.dart'; + +void main() { + // Store the initial instance before any tests change it. + final VideoPlayerPlatform initialInstance = VideoPlayerPlatform.instance; + + test('default implementation throws uninimpletemented', () async { + await expectLater(() => initialInstance.init(), throwsUnimplementedError); + }); +} diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 1cd428c4deea..52cfd319349b 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,36 @@ +## NEXT + +* Updates minimum Flutter version to 2.10. + +## 2.0.12 + +* Updates the `README` with: + * Information about a common known issue: "Some videos restart when using the + seek bar/progress bar/scrubber" (Issue [#49630](https://github.com/flutter/flutter/issues/49360)) + * Links to the Autoplay information of all major browsers (Chrome/Edge, Firefox, Safari). + +## 2.0.11 + +* Improves handling of videos with `Infinity` duration. + +## 2.0.10 + +* Minor fixes for new analysis options. + +## 2.0.9 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 2.0.8 + +* Ensures `buffering` state is only removed when the browser reports enough data + has been buffered so that the video can likely play through without stopping + (`onCanPlayThrough`). Issue [#94630](https://github.com/flutter/flutter/issues/94630). +* Improves testability of the `_VideoPlayer` private class. +* Ensures that tests that listen to a Stream fail "fast" (1 second max timeout). + ## 2.0.7 * Internal code cleanup for stricter analysis options. diff --git a/packages/video_player/video_player_web/README.md b/packages/video_player/video_player_web/README.md index 85e55ebcbe80..ce5d4720ac8e 100644 --- a/packages/video_player/video_player_web/README.md +++ b/packages/video_player/video_player_web/README.md @@ -5,20 +5,56 @@ The web implementation of [`video_player`][1]. ## Usage This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), -which means you can simply use `video_player` -normally. This package will be automatically included in your app when you do. +which means you can simply use `video_player` normally. This package will be +automatically included in your app when you do. -## dart:io +## Limitations on the Web platform -The Web platform does **not** suppport `dart:io`, so attempts to create a `VideoPlayerController.file` will throw an `UnimplementedError`. +Video playback on the Web platform has some limitations that might surprise developers +more familiar with mobile/desktop targets. -## Autoplay -Playing videos without prior interaction with the site might be prohibited -by the browser and lead to runtime errors. See also: https://goo.gl/xX8pDD. +In no particular order: -## Mixing audio with other audio sources +### dart:io -The `VideoPlayerOptions.mixWithOthers` option can't be implemented in web, at least at the moment. If you use this option it will be silently ignored. +The web platform does **not** suppport `dart:io`, so attempts to create a `VideoPlayerController.file` +will throw an `UnimplementedError`. + +### Autoplay + +Attempts to start playing videos with an audio track (or not muted) without user +interaction with the site ("user activation") will be prohibited by the browser +and cause runtime errors in JS. + +See also: + +* [Autoplay policy in Chrome](https://developer.chrome.com/blog/autoplay/) +* MDN > [Autoplay guide for media and Web Audio APIs](https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide) +* Delivering Video Content for Safari > [Enable Video Autoplay](https://developer.apple.com/documentation/webkit/delivering_video_content_for_safari#3030251) +* More info about "user activation", in general: + * [Making user activation consistent across APIs](https://developer.chrome.com/blog/user-activation) + * HTML Spec: [Tracking user activation](https://html.spec.whatwg.org/multipage/interaction.html#sticky-activation) + +### Some videos restart when using the seek bar/progress bar/scrubber + +Certain videos will rewind to the beginning when users attempt to `seekTo` (change +the progress/scrub to) another position, instead of jumping to the desired position. +Once the video is fully stored in the browser cache, seeking will work fine after +a full page reload. + +The most common explanation for this issue is that the server where the video is +stored doesn't support [HTTP range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests). + +> **NOTE:** Flutter web's local server (the one that powers `flutter run`) **DOES NOT** support +> range requests, so all video **assets** in `debug` mode will exhibit this behavior. + +See [Issue #49360](https://github.com/flutter/flutter/issues/49360) for more information +on how to diagnose if a server supports range requests or not. + +### Mixing audio with other audio sources + +The `VideoPlayerOptions.mixWithOthers` option can't be implemented in web, at least +at the moment. If you use this option it will be silently ignored. ## Supported Formats @@ -26,28 +62,35 @@ The `VideoPlayerOptions.mixWithOthers` option can't be implemented in web, at le ### Video codecs? -Check MDN's [**Web video codec guide**](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_codecs) to learn more about the pros and cons of each video codec. +Check MDN's [**Web video codec guide**](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_codecs) +to learn more about the pros and cons of each video codec. ### What codecs are supported? -Visit [**caniuse.com: 'video format'**](https://caniuse.com/#search=video%20format) for a breakdown of which browsers support what codecs. You can customize charts there for the users of your particular website(s). +Visit [**caniuse.com: 'video format'**](https://caniuse.com/#search=video%20format) +for a breakdown of which browsers support what codecs. You can customize charts +there for the users of your particular website(s). Here's an abridged version of the data from caniuse, for a Global audience: #### MPEG-4/H.264 + [![Data on Global support for the MPEG-4/H.264 video format](https://caniuse.bitsofco.de/image/mpeg4.png)](https://caniuse.com/#feat=mpeg4) #### WebM + [![Data on Global support for the WebM video format](https://caniuse.bitsofco.de/image/webm.png)](https://caniuse.com/#feat=webm) #### Ogg/Theora + [![Data on Global support for the Ogg/Theora video format](https://caniuse.bitsofco.de/image/ogv.png)](https://caniuse.com/#feat=ogv) #### AV1 + [![Data on Global support for the AV1 video format](https://caniuse.bitsofco.de/image/av1.png)](https://caniuse.com/#feat=av1) #### HEVC/H.265 -[![Data on Global support for the HEVC/H.265 video format](https://caniuse.bitsofco.de/image/hevc.png)](https://caniuse.com/#feat=hevc) +[![Data on Global support for the HEVC/H.265 video format](https://caniuse.bitsofco.de/image/hevc.png)](https://caniuse.com/#feat=hevc) [1]: ../video_player diff --git a/packages/video_player/video_player_web/example/README.md b/packages/video_player/video_player_web/example/README.md index 8a6e74b107ea..0e51ae5ecbd2 100644 --- a/packages/video_player/video_player_web/example/README.md +++ b/packages/video_player/video_player_web/example/README.md @@ -1,4 +1,14 @@ -# Testing +# 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. + +## Testing This package uses `package:integration_test` to run its tests in a web browser. @@ -6,4 +16,4 @@ See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Te in the Flutter wiki for instructions to setup and run the tests in this package. Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) -for more info. \ No newline at end of file +for more info. diff --git a/packages/video_player/video_player_web/example/integration_test/duration_utils_test.dart b/packages/video_player/video_player_web/example/integration_test/duration_utils_test.dart new file mode 100644 index 000000000000..c0d639843833 --- /dev/null +++ b/packages/video_player/video_player_web/example/integration_test/duration_utils_test.dart @@ -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. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:video_player_web/src/duration_utils.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('convertNumVideoDurationToPluginDuration', () { + testWidgets('Finite value converts to milliseconds', + (WidgetTester _) async { + final Duration? result = convertNumVideoDurationToPluginDuration(1.5); + final Duration? zero = convertNumVideoDurationToPluginDuration(0.0001); + + expect(result, isNotNull); + expect(result!.inMilliseconds, equals(1500)); + expect(zero, equals(Duration.zero)); + }); + + testWidgets('Finite value rounds 3rd decimal value', + (WidgetTester _) async { + final Duration? result = + convertNumVideoDurationToPluginDuration(1.567899089087); + final Duration? another = + convertNumVideoDurationToPluginDuration(1.567199089087); + + expect(result, isNotNull); + expect(result!.inMilliseconds, equals(1568)); + expect(another!.inMilliseconds, equals(1567)); + }); + + testWidgets('Infinite value returns magic constant', + (WidgetTester _) async { + final Duration? result = + convertNumVideoDurationToPluginDuration(double.infinity); + + expect(result, isNotNull); + expect(result, equals(jsCompatibleTimeUnset)); + expect(result!.inMilliseconds, equals(-9007199254740990)); + }); + + testWidgets('NaN value returns null', (WidgetTester _) async { + final Duration? result = + convertNumVideoDurationToPluginDuration(double.nan); + + expect(result, isNull); + }); + }); +} diff --git a/packages/video_player/video_player_web/example/integration_test/utils.dart b/packages/video_player/video_player_web/example/integration_test/utils.dart new file mode 100644 index 000000000000..2bb234ea3660 --- /dev/null +++ b/packages/video_player/video_player_web/example/integration_test/utils.dart @@ -0,0 +1,56 @@ +// 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 integration_test_utils; + +import 'dart:html'; + +import 'package:js/js.dart'; + +// Returns the URL to load an asset from this example app as a network source. +// +// TODO(stuartmorgan): Convert this to a local `HttpServer` that vends the +// assets directly, https://github.com/flutter/flutter/issues/95420 +String getUrlForAssetAsNetworkSource(String assetKey) { + return 'https://github.com/flutter/plugins/blob/' + // This hash can be rolled forward to pick up newly-added assets. + 'cb381ced070d356799dddf24aca38ce0579d3d7b' + '/packages/video_player/video_player/example/' + '$assetKey' + '?raw=true'; +} + +@JS() +@anonymous +class _Descriptor { + // May also contain "configurable" and "enumerable" bools. + // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#description + external factory _Descriptor({ + // bool configurable, + // bool enumerable, + bool writable, + Object value, + }); +} + +@JS('Object.defineProperty') +external void _defineProperty( + Object object, + String property, + _Descriptor description, +); + +/// Forces a VideoElement to report "Infinity" duration. +/// +/// Uses JS Object.defineProperty to set the value of a readonly property. +void setInfinityDuration(VideoElement element) { + _defineProperty( + element, + 'duration', + _Descriptor( + writable: true, + value: double.infinity, + )); +} diff --git a/packages/video_player/video_player_web/example/integration_test/video_player_test.dart b/packages/video_player/video_player_web/example/integration_test/video_player_test.dart new file mode 100644 index 000000000000..28046f42e9a8 --- /dev/null +++ b/packages/video_player/video_player_web/example/integration_test/video_player_test.dart @@ -0,0 +1,217 @@ +// 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:html' as html; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; +import 'package:video_player_web/src/duration_utils.dart'; +import 'package:video_player_web/src/video_player.dart'; + +import 'utils.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('VideoPlayer', () { + late html.VideoElement video; + + setUp(() { + // Never set "src" on the video, so this test doesn't hit the network! + video = html.VideoElement() + ..controls = true + ..setAttribute('playsinline', 'false'); + }); + + testWidgets('fixes critical video element config', (WidgetTester _) async { + VideoPlayer(videoElement: video).initialize(); + + expect(video.controls, isFalse, + reason: 'Video is controlled through code'); + expect(video.getAttribute('autoplay'), 'false', + reason: 'Cannot autoplay on the web'); + expect(video.getAttribute('playsinline'), 'true', + reason: 'Needed by safari iOS'); + }); + + testWidgets('setVolume', (WidgetTester tester) async { + final VideoPlayer player = VideoPlayer(videoElement: video)..initialize(); + + player.setVolume(0); + + expect(video.volume, isZero, reason: 'Volume should be zero'); + expect(video.muted, isTrue, reason: 'muted attribute should be true'); + + expect(() { + player.setVolume(-0.0001); + }, throwsAssertionError, reason: 'Volume cannot be < 0'); + + expect(() { + player.setVolume(1.0001); + }, throwsAssertionError, reason: 'Volume cannot be > 1'); + }); + + testWidgets('setPlaybackSpeed', (WidgetTester tester) async { + final VideoPlayer player = VideoPlayer(videoElement: video)..initialize(); + + expect(() { + player.setPlaybackSpeed(-1); + }, throwsAssertionError, reason: 'Playback speed cannot be < 0'); + + expect(() { + player.setPlaybackSpeed(0); + }, throwsAssertionError, reason: 'Playback speed cannot be == 0'); + }); + + testWidgets('seekTo', (WidgetTester tester) async { + final VideoPlayer player = VideoPlayer(videoElement: video)..initialize(); + + expect(() { + player.seekTo(const Duration(seconds: -1)); + }, throwsAssertionError, reason: 'Cannot seek into negative numbers'); + }); + + // The events tested in this group do *not* represent the actual sequence + // of events from a real "video" element. They're crafted to test the + // behavior of the VideoPlayer in different states with different events. + group('events', () { + late StreamController streamController; + late VideoPlayer player; + late Stream timedStream; + + final Set bufferingEvents = { + VideoEventType.bufferingStart, + VideoEventType.bufferingEnd, + }; + + setUp(() { + streamController = StreamController(); + player = + VideoPlayer(videoElement: video, eventController: streamController) + ..initialize(); + + // This stream will automatically close after 100 ms without seeing any events + timedStream = streamController.stream.timeout( + const Duration(milliseconds: 100), + onTimeout: (EventSink sink) { + sink.close(); + }, + ); + }); + + testWidgets('buffering dispatches only when it changes', + (WidgetTester tester) async { + // Take all the "buffering" events that we see during the next few seconds + final Future> stream = timedStream + .where( + (VideoEvent event) => bufferingEvents.contains(event.eventType)) + .map((VideoEvent event) => + event.eventType == VideoEventType.bufferingStart) + .toList(); + + // Simulate some events coming from the player... + player.setBuffering(true); + player.setBuffering(true); + player.setBuffering(true); + player.setBuffering(false); + player.setBuffering(false); + player.setBuffering(true); + player.setBuffering(false); + player.setBuffering(true); + player.setBuffering(false); + + final List events = await stream; + + expect(events, hasLength(6)); + expect(events, [true, false, true, false, true, false]); + }); + + testWidgets('canplay event does not change buffering state', + (WidgetTester tester) async { + // Take all the "buffering" events that we see during the next few seconds + final Future> stream = timedStream + .where( + (VideoEvent event) => bufferingEvents.contains(event.eventType)) + .map((VideoEvent event) => + event.eventType == VideoEventType.bufferingStart) + .toList(); + + player.setBuffering(true); + + // Simulate "canplay" event... + video.dispatchEvent(html.Event('canplay')); + + final List events = await stream; + + expect(events, hasLength(1)); + expect(events, [true]); + }); + + testWidgets('canplaythrough event does change buffering state', + (WidgetTester tester) async { + // Take all the "buffering" events that we see during the next few seconds + final Future> stream = timedStream + .where( + (VideoEvent event) => bufferingEvents.contains(event.eventType)) + .map((VideoEvent event) => + event.eventType == VideoEventType.bufferingStart) + .toList(); + + player.setBuffering(true); + + // Simulate "canplaythrough" event... + video.dispatchEvent(html.Event('canplaythrough')); + + final List events = await stream; + + expect(events, hasLength(2)); + expect(events, [true, false]); + }); + + testWidgets('initialized dispatches only once', + (WidgetTester tester) async { + // Dispatch some bogus "canplay" events from the video object + video.dispatchEvent(html.Event('canplay')); + video.dispatchEvent(html.Event('canplay')); + video.dispatchEvent(html.Event('canplay')); + + // Take all the "initialized" events that we see during the next few seconds + final Future> stream = timedStream + .where((VideoEvent event) => + event.eventType == VideoEventType.initialized) + .toList(); + + video.dispatchEvent(html.Event('canplay')); + video.dispatchEvent(html.Event('canplay')); + video.dispatchEvent(html.Event('canplay')); + + final List events = await stream; + + expect(events, hasLength(1)); + expect(events[0].eventType, VideoEventType.initialized); + }); + + // Issue: https://github.com/flutter/flutter/issues/105649 + testWidgets('supports `Infinity` duration', (WidgetTester _) async { + setInfinityDuration(video); + expect(video.duration.isInfinite, isTrue); + + final Future> stream = timedStream + .where((VideoEvent event) => + event.eventType == VideoEventType.initialized) + .toList(); + + video.dispatchEvent(html.Event('canplay')); + + final List events = await stream; + + expect(events, hasLength(1)); + expect(events[0].eventType, VideoEventType.initialized); + expect(events[0].duration, equals(jsCompatibleTimeUnset)); + }); + }); + }); +} diff --git a/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart b/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart index 97b03642cd07..5053ea6e5b04 100644 --- a/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart +++ b/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart @@ -11,10 +11,15 @@ import 'package:integration_test/integration_test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'package:video_player_web/video_player_web.dart'; +import 'utils.dart'; + +// Use WebM to allow CI to run tests in Chromium. +const String _videoAssetKey = 'assets/Butterfly-209.webm'; + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('VideoPlayer for Web', () { + group('VideoPlayerWeb plugin (hits network)', () { late Future textureId; setUp(() { @@ -23,8 +28,7 @@ void main() { .create( DataSource( sourceType: DataSourceType.network, - uri: - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + uri: getUrlForAssetAsNetworkSource(_videoAssetKey), ), ) .then((int? textureId) => textureId!); @@ -38,9 +42,9 @@ void main() { expect( VideoPlayerPlatform.instance.create( DataSource( - sourceType: DataSourceType.network, - uri: - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'), + sourceType: DataSourceType.network, + uri: getUrlForAssetAsNetworkSource(_videoAssetKey), + ), ), completion(isNonZero)); }); @@ -100,9 +104,9 @@ void main() { (WidgetTester tester) async { final int videoPlayerId = (await VideoPlayerPlatform.instance.create( DataSource( - sourceType: DataSourceType.network, - uri: - 'https://flutter.github.io/assets-for-api-docs/assets/videos/_non_existent_video.mp4'), + sourceType: DataSourceType.network, + uri: getUrlForAssetAsNetworkSource('assets/__non_existent.webm'), + ), ))!; final Stream eventStream = @@ -113,7 +117,7 @@ void main() { await VideoPlayerPlatform.instance.play(videoPlayerId); expect(() async { - await eventStream.last; + await eventStream.timeout(const Duration(seconds: 5)).last; }, throwsA(isA())); }); @@ -164,5 +168,40 @@ void main() { expect(VideoPlayerPlatform.instance.setMixWithOthers(true), completes); expect(VideoPlayerPlatform.instance.setMixWithOthers(false), completes); }); + + testWidgets('video playback lifecycle', (WidgetTester tester) async { + final int videoPlayerId = await textureId; + final Stream eventStream = + VideoPlayerPlatform.instance.videoEventsFor(videoPlayerId); + + final Future> stream = eventStream.timeout( + const Duration(seconds: 1), + onTimeout: (EventSink sink) { + sink.close(); + }, + ).toList(); + + await VideoPlayerPlatform.instance.setVolume(videoPlayerId, 0); + await VideoPlayerPlatform.instance.play(videoPlayerId); + + // Let the video play, until we stop seeing events for a second + final List events = await stream; + + await VideoPlayerPlatform.instance.pause(videoPlayerId); + + // The expected list of event types should look like this: + // 1. bufferingStart, + // 2. bufferingUpdate (videoElement.onWaiting), + // 3. initialized (videoElement.onCanPlay), + // 4. bufferingEnd (videoElement.onCanPlayThrough), + expect( + events.map((VideoEvent e) => e.eventType), + equals([ + VideoEventType.bufferingStart, + VideoEventType.bufferingUpdate, + VideoEventType.initialized, + VideoEventType.bufferingEnd + ])); + }); }); } diff --git a/packages/video_player/video_player_web/example/lib/main.dart b/packages/video_player/video_player_web/example/lib/main.dart index 341913a18490..87422953de6a 100644 --- a/packages/video_player/video_player_web/example/lib/main.dart +++ b/packages/video_player/video_player_web/example/lib/main.dart @@ -5,13 +5,16 @@ import 'package:flutter/material.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } /// App for testing class MyApp extends StatefulWidget { + /// Default Constructor + const MyApp({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index bc70b7d21aca..57728f323d55 100644 --- a/packages/video_player/video_player_web/example/pubspec.yaml +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -3,11 +3,13 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.2.0" + flutter: ">=2.10.0" dependencies: flutter: sdk: flutter + js: ^0.6.0 + video_player_platform_interface: ">=4.2.0 <6.0.0" video_player_web: path: ../ diff --git a/packages/video_player/video_player_web/lib/src/duration_utils.dart b/packages/video_player/video_player_web/lib/src/duration_utils.dart new file mode 100644 index 000000000000..030d6b040988 --- /dev/null +++ b/packages/video_player/video_player_web/lib/src/duration_utils.dart @@ -0,0 +1,33 @@ +// 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 "length" of a video which doesn't have finite duration. +// See: https://github.com/flutter/flutter/issues/107882 +const Duration jsCompatibleTimeUnset = Duration( + milliseconds: -9007199254740990, // Number.MIN_SAFE_INTEGER + 1. -(2^53 - 1) +); + +/// Converts a `num` duration coming from a [VideoElement] into a [Duration] that +/// the plugin can use. +/// +/// From the documentation, `videoDuration` is "a double-precision floating-point +/// value indicating the duration of the media in seconds. +/// If no media data is available, the value `NaN` is returned. +/// If the element's media doesn't have a known duration —such as for live media +/// streams— the value of duration is `+Infinity`." +/// +/// If the `videoDuration` is finite, this method returns it as a `Duration`. +/// If the `videoDuration` is `Infinity`, the duration will be +/// `-9007199254740990` milliseconds. (See https://github.com/flutter/flutter/issues/107882) +/// If the `videoDuration` is `NaN`, this will return null. +Duration? convertNumVideoDurationToPluginDuration(num duration) { + if (duration.isFinite) { + return Duration( + milliseconds: (duration * 1000).round(), + ); + } else if (duration.isInfinite) { + return jsCompatibleTimeUnset; + } + return null; +} diff --git a/packages/video_player/video_player_web/lib/src/shims/dart_ui_fake.dart b/packages/video_player/video_player_web/lib/src/shims/dart_ui_fake.dart index 8757ca22be17..40d8f1903111 100644 --- a/packages/video_player/video_player_web/lib/src/shims/dart_ui_fake.dart +++ b/packages/video_player/video_player_web/lib/src/shims/dart_ui_fake.dart @@ -11,10 +11,10 @@ import 'dart:html' as html; // ignore_for_file: camel_case_types /// Shim for web_ui engine.PlatformViewRegistry -/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62 +/// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/ui.dart#L62 class platformViewRegistry { /// Shim for registerViewFactory - /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72 + /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/ui.dart#L72 static bool registerViewFactory( String viewTypeId, html.Element Function(int viewId) viewFactory) { return false; @@ -22,10 +22,10 @@ class platformViewRegistry { } /// Shim for web_ui engine.AssetManager. -/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12 +/// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L12 class webOnlyAssetManager { /// Shim for getAssetUrl. - /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L45 + /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L45 static String getAssetUrl(String asset) => ''; } diff --git a/packages/video_player/video_player_web/lib/src/video_player.dart b/packages/video_player/video_player_web/lib/src/video_player.dart new file mode 100644 index 000000000000..02ead1fdf93b --- /dev/null +++ b/packages/video_player/video_player_web/lib/src/video_player.dart @@ -0,0 +1,253 @@ +// 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:html' as html; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +import 'duration_utils.dart'; + +// An error code value to error name Map. +// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code +const Map _kErrorValueToErrorName = { + 1: 'MEDIA_ERR_ABORTED', + 2: 'MEDIA_ERR_NETWORK', + 3: 'MEDIA_ERR_DECODE', + 4: 'MEDIA_ERR_SRC_NOT_SUPPORTED', +}; + +// An error code value to description Map. +// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code +const Map _kErrorValueToErrorDescription = { + 1: 'The user canceled the fetching of the video.', + 2: 'A network error occurred while fetching the video, despite having previously been available.', + 3: 'An error occurred while trying to decode the video, despite having previously been determined to be usable.', + 4: 'The video has been found to be unsuitable (missing or in a format not supported by your browser).', +}; + +// The default error message, when the error is an empty string +// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/message +const String _kDefaultErrorMessage = + 'No further diagnostic information can be determined or provided.'; + +/// Wraps a [html.VideoElement] so its API complies with what is expected by the plugin. +class VideoPlayer { + /// Create a [VideoPlayer] from a [html.VideoElement] instance. + VideoPlayer({ + required html.VideoElement videoElement, + @visibleForTesting StreamController? eventController, + }) : _videoElement = videoElement, + _eventController = eventController ?? StreamController(); + + final StreamController _eventController; + final html.VideoElement _videoElement; + + bool _isInitialized = false; + bool _isBuffering = false; + + /// Returns the [Stream] of [VideoEvent]s from the inner [html.VideoElement]. + Stream get events => _eventController.stream; + + /// Initializes the wrapped [html.VideoElement]. + /// + /// This method sets the required DOM attributes so videos can [play] programmatically, + /// and attaches listeners to the internal events from the [html.VideoElement] + /// to react to them / expose them through the [VideoPlayer.events] stream. + void initialize() { + _videoElement + ..autoplay = false + ..controls = false; + + // Allows Safari iOS to play the video inline + _videoElement.setAttribute('playsinline', 'true'); + + // Set autoplay to false since most browsers won't autoplay a video unless it is muted + _videoElement.setAttribute('autoplay', 'false'); + + _videoElement.onCanPlay.listen((dynamic _) { + if (!_isInitialized) { + _isInitialized = true; + _sendInitialized(); + } + }); + + _videoElement.onCanPlayThrough.listen((dynamic _) { + setBuffering(false); + }); + + _videoElement.onPlaying.listen((dynamic _) { + setBuffering(false); + }); + + _videoElement.onWaiting.listen((dynamic _) { + setBuffering(true); + _sendBufferingRangesUpdate(); + }); + + // The error event fires when some form of error occurs while attempting to load or perform the media. + _videoElement.onError.listen((html.Event _) { + setBuffering(false); + // The Event itself (_) doesn't contain info about the actual error. + // We need to look at the HTMLMediaElement.error. + // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error + final html.MediaError error = _videoElement.error!; + _eventController.addError(PlatformException( + code: _kErrorValueToErrorName[error.code]!, + message: error.message != '' ? error.message : _kDefaultErrorMessage, + details: _kErrorValueToErrorDescription[error.code], + )); + }); + + _videoElement.onEnded.listen((dynamic _) { + setBuffering(false); + _eventController.add(VideoEvent(eventType: VideoEventType.completed)); + }); + } + + /// Attempts to play the video. + /// + /// If this method is called programmatically (without user interaction), it + /// might fail unless the video is completely muted (or it has no Audio tracks). + /// + /// When called from some user interaction (a tap on a button), the above + /// limitation should disappear. + Future play() { + return _videoElement.play().catchError((Object e) { + // play() attempts to begin playback of the media. It returns + // a Promise which can get rejected in case of failure to begin + // playback for any reason, such as permission issues. + // The rejection handler is called with a DomException. + // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play + final html.DomException exception = e as html.DomException; + _eventController.addError(PlatformException( + code: exception.name, + message: exception.message, + )); + }, test: (Object e) => e is html.DomException); + } + + /// Pauses the video in the current position. + void pause() { + _videoElement.pause(); + } + + /// Controls whether the video should start again after it finishes. + // ignore: use_setters_to_change_properties + void setLooping(bool value) { + _videoElement.loop = value; + } + + /// Sets the volume at which the media will be played. + /// + /// Values must fall between 0 and 1, where 0 is muted and 1 is the loudest. + /// + /// When volume is set to 0, the `muted` property is also applied to the + /// [html.VideoElement]. This is required for auto-play on the web. + void setVolume(double volume) { + assert(volume >= 0 && volume <= 1); + + // TODO(ditman): Do we need to expose a "muted" API? + // https://github.com/flutter/flutter/issues/60721 + _videoElement.muted = !(volume > 0.0); + _videoElement.volume = volume; + } + + /// Sets the playback `speed`. + /// + /// A `speed` of 1.0 is "normal speed," values lower than 1.0 make the media + /// play slower than normal, higher values make it play faster. + /// + /// `speed` cannot be negative. + /// + /// The audio is muted when the fast forward or slow motion is outside a useful + /// range (for example, Gecko mutes the sound outside the range 0.25 to 4.0). + /// + /// The pitch of the audio is corrected by default. + void setPlaybackSpeed(double speed) { + assert(speed > 0); + + _videoElement.playbackRate = speed; + } + + /// Moves the playback head to a new `position`. + /// + /// `position` cannot be negative. + void seekTo(Duration position) { + assert(!position.isNegative); + + _videoElement.currentTime = position.inMilliseconds.toDouble() / 1000; + } + + /// Returns the current playback head position as a [Duration]. + Duration getPosition() { + _sendBufferingRangesUpdate(); + return Duration(milliseconds: (_videoElement.currentTime * 1000).round()); + } + + /// Disposes of the current [html.VideoElement]. + void dispose() { + _videoElement.removeAttribute('src'); + _videoElement.load(); + } + + // Sends an [VideoEventType.initialized] [VideoEvent] with info about the wrapped video. + void _sendInitialized() { + final Duration? duration = + convertNumVideoDurationToPluginDuration(_videoElement.duration); + + final Size? size = _videoElement.videoHeight.isFinite + ? Size( + _videoElement.videoWidth.toDouble(), + _videoElement.videoHeight.toDouble(), + ) + : null; + + _eventController.add( + VideoEvent( + eventType: VideoEventType.initialized, + duration: duration, + size: size, + ), + ); + } + + /// Caches the current "buffering" state of the video. + /// + /// If the current buffering state is different from the previous one + /// ([_isBuffering]), this dispatches a [VideoEvent]. + @visibleForTesting + void setBuffering(bool buffering) { + if (_isBuffering != buffering) { + _isBuffering = buffering; + _eventController.add(VideoEvent( + eventType: _isBuffering + ? VideoEventType.bufferingStart + : VideoEventType.bufferingEnd, + )); + } + } + + // Broadcasts the [html.VideoElement.buffered] status through the [events] stream. + void _sendBufferingRangesUpdate() { + _eventController.add(VideoEvent( + buffered: _toDurationRange(_videoElement.buffered), + eventType: VideoEventType.bufferingUpdate, + )); + } + + // Converts from [html.TimeRanges] to our own List. + List _toDurationRange(html.TimeRanges buffered) { + final List durationRange = []; + for (int i = 0; i < buffered.length; i++) { + durationRange.add(DurationRange( + Duration(milliseconds: (buffered.start(i) * 1000).round()), + Duration(milliseconds: (buffered.end(i) * 1000).round()), + )); + } + return durationRange; + } +} diff --git a/packages/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart index a676850f3488..e52fd83de79e 100644 --- a/packages/video_player/video_player_web/lib/video_player_web.dart +++ b/packages/video_player/video_player_web/lib/video_player_web.dart @@ -6,34 +6,11 @@ import 'dart:async'; import 'dart:html'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'src/shims/dart_ui.dart' as ui; - -// An error code value to error name Map. -// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code -const Map _kErrorValueToErrorName = { - 1: 'MEDIA_ERR_ABORTED', - 2: 'MEDIA_ERR_NETWORK', - 3: 'MEDIA_ERR_DECODE', - 4: 'MEDIA_ERR_SRC_NOT_SUPPORTED', -}; - -// An error code value to description Map. -// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code -const Map _kErrorValueToErrorDescription = { - 1: 'The user canceled the fetching of the video.', - 2: 'A network error occurred while fetching the video, despite having previously been available.', - 3: 'An error occurred while trying to decode the video, despite having previously been determined to be usable.', - 4: 'The video has been found to be unsuitable (missing or in a format not supported by your browser).', -}; - -// The default error message, when the error is an empty string -// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/message -const String _kDefaultErrorMessage = - 'No further diagnostic information can be determined or provided.'; +import 'src/video_player.dart'; /// The web implementation of [VideoPlayerPlatform]. /// @@ -44,8 +21,10 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { VideoPlayerPlatform.instance = VideoPlayerPlugin(); } - final Map _videoPlayers = {}; + // Map of textureId -> VideoPlayer instances + final Map _videoPlayers = {}; + // Simulate the native "textureId". int _textureCounter = 1; @override @@ -55,13 +34,13 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { @override Future dispose(int textureId) async { - _videoPlayers[textureId]!.dispose(); + _player(textureId).dispose(); _videoPlayers.remove(textureId); return; } void _disposeAllPlayers() { - for (final _VideoPlayer videoPlayer in _videoPlayers.values) { + for (final VideoPlayer videoPlayer in _videoPlayers.values) { videoPlayer.dispose(); } _videoPlayers.clear(); @@ -69,8 +48,7 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { @override Future create(DataSource dataSource) async { - final int textureId = _textureCounter; - _textureCounter++; + final int textureId = _textureCounter++; late String uri; switch (dataSource.sourceType) { @@ -95,58 +73,69 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { 'web implementation of video_player cannot play content uri')); } - final _VideoPlayer player = _VideoPlayer( - uri: uri, - textureId: textureId, - ); + final VideoElement videoElement = VideoElement() + ..id = 'videoElement-$textureId' + ..src = uri + ..style.border = 'none' + ..style.height = '100%' + ..style.width = '100%'; - player.initialize(); + // TODO(hterkelsen): Use initialization parameters once they are available + ui.platformViewRegistry.registerViewFactory( + 'videoPlayer-$textureId', (int viewId) => videoElement); + + final VideoPlayer player = VideoPlayer(videoElement: videoElement) + ..initialize(); _videoPlayers[textureId] = player; + return textureId; } @override Future setLooping(int textureId, bool looping) async { - return _videoPlayers[textureId]!.setLooping(looping); + return _player(textureId).setLooping(looping); } @override Future play(int textureId) async { - return _videoPlayers[textureId]!.play(); + return _player(textureId).play(); } @override Future pause(int textureId) async { - return _videoPlayers[textureId]!.pause(); + return _player(textureId).pause(); } @override Future setVolume(int textureId, double volume) async { - return _videoPlayers[textureId]!.setVolume(volume); + return _player(textureId).setVolume(volume); } @override Future setPlaybackSpeed(int textureId, double speed) async { - assert(speed > 0); - - return _videoPlayers[textureId]!.setPlaybackSpeed(speed); + return _player(textureId).setPlaybackSpeed(speed); } @override Future seekTo(int textureId, Duration position) async { - return _videoPlayers[textureId]!.seekTo(position); + return _player(textureId).seekTo(position); } @override Future getPosition(int textureId) async { - _videoPlayers[textureId]!.sendBufferingUpdate(); - return _videoPlayers[textureId]!.getPosition(); + return _player(textureId).getPosition(); } @override Stream videoEventsFor(int textureId) { - return _videoPlayers[textureId]!.eventController.stream; + return _player(textureId).events; + } + + // Retrieves a [VideoPlayer] by its internal `id`. + // It must have been created earlier from the [create] method. + VideoPlayer _player(int id) { + return _videoPlayers[id]!; } @override @@ -158,171 +147,3 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { @override Future setMixWithOthers(bool mixWithOthers) => Future.value(); } - -class _VideoPlayer { - _VideoPlayer({required this.uri, required this.textureId}); - - final StreamController eventController = - StreamController(); - - final String uri; - final int textureId; - late VideoElement videoElement; - bool isInitialized = false; - bool isBuffering = false; - - void setBuffering(bool buffering) { - if (isBuffering != buffering) { - isBuffering = buffering; - eventController.add(VideoEvent( - eventType: isBuffering - ? VideoEventType.bufferingStart - : VideoEventType.bufferingEnd)); - } - } - - void initialize() { - videoElement = VideoElement() - ..src = uri - ..autoplay = false - ..controls = false - ..style.border = 'none' - ..style.height = '100%' - ..style.width = '100%'; - - // Allows Safari iOS to play the video inline - videoElement.setAttribute('playsinline', 'true'); - - // Set autoplay to false since most browsers won't autoplay a video unless it is muted - videoElement.setAttribute('autoplay', 'false'); - - // TODO(hterkelsen): Use initialization parameters once they are available - ui.platformViewRegistry.registerViewFactory( - 'videoPlayer-$textureId', (int viewId) => videoElement); - - videoElement.onCanPlay.listen((dynamic _) { - if (!isInitialized) { - isInitialized = true; - sendInitialized(); - } - setBuffering(false); - }); - - videoElement.onCanPlayThrough.listen((dynamic _) { - setBuffering(false); - }); - - videoElement.onPlaying.listen((dynamic _) { - setBuffering(false); - }); - - videoElement.onWaiting.listen((dynamic _) { - setBuffering(true); - sendBufferingUpdate(); - }); - - // The error event fires when some form of error occurs while attempting to load or perform the media. - videoElement.onError.listen((Event _) { - setBuffering(false); - // The Event itself (_) doesn't contain info about the actual error. - // We need to look at the HTMLMediaElement.error. - // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error - final MediaError error = videoElement.error!; - eventController.addError(PlatformException( - code: _kErrorValueToErrorName[error.code]!, - message: error.message != '' ? error.message : _kDefaultErrorMessage, - details: _kErrorValueToErrorDescription[error.code], - )); - }); - - videoElement.onEnded.listen((dynamic _) { - setBuffering(false); - eventController.add(VideoEvent(eventType: VideoEventType.completed)); - }); - } - - void sendBufferingUpdate() { - eventController.add(VideoEvent( - buffered: _toDurationRange(videoElement.buffered), - eventType: VideoEventType.bufferingUpdate, - )); - } - - Future play() { - return videoElement.play().catchError((Object e) { - // play() attempts to begin playback of the media. It returns - // a Promise which can get rejected in case of failure to begin - // playback for any reason, such as permission issues. - // The rejection handler is called with a DomException. - // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play - final DomException exception = e as DomException; - eventController.addError(PlatformException( - code: exception.name, - message: exception.message, - )); - }, test: (Object e) => e is DomException); - } - - void pause() { - videoElement.pause(); - } - - void setLooping(bool value) { - videoElement.loop = value; - } - - void setVolume(double value) { - // TODO(ditman): Do we need to expose a "muted" API? https://github.com/flutter/flutter/issues/60721 - if (value > 0.0) { - videoElement.muted = false; - } else { - videoElement.muted = true; - } - videoElement.volume = value; - } - - void setPlaybackSpeed(double speed) { - assert(speed > 0); - - videoElement.playbackRate = speed; - } - - void seekTo(Duration position) { - videoElement.currentTime = position.inMilliseconds.toDouble() / 1000; - } - - Duration getPosition() { - return Duration(milliseconds: (videoElement.currentTime * 1000).round()); - } - - void sendInitialized() { - eventController.add( - VideoEvent( - eventType: VideoEventType.initialized, - duration: Duration( - milliseconds: (videoElement.duration * 1000).round(), - ), - size: Size( - videoElement.videoWidth.toDouble(), - videoElement.videoHeight.toDouble(), - ), - ), - ); - } - - void dispose() { - videoElement.removeAttribute('src'); - videoElement.load(); - } - - List _toDurationRange(TimeRanges buffered) { - final List durationRange = []; - for (int i = 0; i < buffered.length; i++) { - durationRange.add(DurationRange( - Duration(milliseconds: (buffered.start(i) * 1000).round()), - Duration(milliseconds: (buffered.end(i) * 1000).round()), - )); - } - return durationRange; - } -} diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index 69a2df4e99e4..ad6c9bfa198f 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -2,11 +2,11 @@ name: video_player_web description: Web platform implementation of video_player. repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.0.7 +version: 2.0.12 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 547630249c9d..be6a21321ea1 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,26 @@ +## NEXT + +* 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. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/104231). +* Updates references to the obsolete master branch. + +## 3.0.4 + +* Minor fixes for new analysis options. + +## 3.0.3 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 3.0.2 + +* Migrates deprecated `Scaffold.showSnackBar` to `ScaffoldMessenger` in example app. +* Adds OS version support information to README. + ## 3.0.1+4 * Updates to webview_flutter_wkwebview version 2.7.1+3. @@ -17,12 +40,11 @@ * Support Geolocation. * Support select pdf|doc|xls files. - ## 3.0.1 * Removes a duplicate Android-specific integration test. * Fixes an integration test race condition. -* Fixes comments (accidentially mixed // with ///). +* Fixes comments (accidentally mixed // with ///). ## 3.0.0 @@ -191,7 +213,7 @@ when hybrid composition is used [flutter/issues/75667](https://github.com/flutte performing better on iOS. Flutter 1.22 no longer requires adding the `io.flutter.embedded_views_preview` flag to `Info.plist`. -* Added support for Hybrid Composition on Android (see opt-in instructions in [README](https://github.com/flutter/plugins/blob/master/packages/webview_flutter/README.md#android)) +* Added support for Hybrid Composition on Android (see opt-in instructions in [README](https://github.com/flutter/plugins/blob/main/packages/webview_flutter/README.md#android)) * Lowered the required Android API to 19 (was previously 20): [#23728](https://github.com/flutter/flutter/issues/23728). * Fixed the following issues: * 🎹 Keyboard: [#41089](https://github.com/flutter/flutter/issues/41089), [#36478](https://github.com/flutter/flutter/issues/36478), [#51254](https://github.com/flutter/flutter/issues/51254), [#50716](https://github.com/flutter/flutter/issues/50716), [#55724](https://github.com/flutter/flutter/issues/55724), [#56513](https://github.com/flutter/flutter/issues/56513), [#56515](https://github.com/flutter/flutter/issues/56515), [#61085](https://github.com/flutter/flutter/issues/61085), [#62205](https://github.com/flutter/flutter/issues/62205), [#62547](https://github.com/flutter/flutter/issues/62547), [#58943](https://github.com/flutter/flutter/issues/58943), [#56361](https://github.com/flutter/flutter/issues/56361), [#56361](https://github.com/flutter/flutter/issues/42902), [#40716](https://github.com/flutter/flutter/issues/40716), [#37989](https://github.com/flutter/flutter/issues/37989), [#27924](https://github.com/flutter/flutter/issues/27924). diff --git a/packages/webview_flutter/webview_flutter/README.md b/packages/webview_flutter/webview_flutter/README.md index c81107845729..1b9630167879 100644 --- a/packages/webview_flutter/webview_flutter/README.md +++ b/packages/webview_flutter/webview_flutter/README.md @@ -159,4 +159,4 @@ follow the steps described in the [Enabling Material Components instructions](ht ### Setting custom headers on POST requests Currently, setting custom headers when making a post request with the WebViewController's `loadRequest` method is not supported on Android. -If you require this functionality, a workaround is to make the request manually, and then load the response data using `loadHTMLString` instead. +If you require this functionality, a workaround is to make the request manually, and then load the response data using `loadHTMLString` instead. \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter/example/README.md b/packages/webview_flutter/webview_flutter/example/README.md index 850ee74397a9..e5bd6e20db63 100644 --- a/packages/webview_flutter/webview_flutter/example/README.md +++ b/packages/webview_flutter/webview_flutter/example/README.md @@ -1,8 +1,3 @@ # webview_flutter_example Demonstrates how to use the webview_flutter plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/webview_flutter/webview_flutter/example/android/app/build.gradle b/packages/webview_flutter/webview_flutter/example/android/app/build.gradle index 532b2adde9d6..968eed6cad85 100644 --- a/packages/webview_flutter/webview_flutter/example/android/app/build.gradle +++ b/packages/webview_flutter/webview_flutter/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 32 lintOptions { disable 'InvalidPackage' @@ -55,7 +55,7 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' api 'androidx.test:core:1.2.0' diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java deleted file mode 100644 index 0b3eeef9b6b7..000000000000 --- a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java +++ /dev/null @@ -1,23 +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. - -package io.flutter.plugins.webviewflutterexample; - -import static org.junit.Assert.assertTrue; - -import androidx.test.core.app.ActivityScenario; -import io.flutter.plugins.webviewflutter.WebViewFlutterPlugin; -import org.junit.Test; - -public class WebViewTest { - @Test - public void webViewPluginIsAdded() { - final ActivityScenario scenario = - ActivityScenario.launch(WebViewTestActivity.class); - scenario.onActivity( - activity -> { - assertTrue(activity.engine.getPlugins().has(WebViewFlutterPlugin.class)); - }); - } -} diff --git a/packages/webview_flutter/webview_flutter/example/android/build.gradle b/packages/webview_flutter/webview_flutter/example/android/build.gradle index eb3a80b1a269..ac477c248fc4 100644 --- a/packages/webview_flutter/webview_flutter/example/android/build.gradle +++ b/packages/webview_flutter/webview_flutter/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath "com.android.tools.build:gradle:7.0.3" + classpath 'com.android.tools.build:gradle:7.2.2' } } diff --git a/packages/webview_flutter/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties index ed1a787b3b22..cc5527d781a7 100644 --- a/packages/webview_flutter/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/webview_flutter/webview_flutter/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-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart index 48df88bba466..0f4c84a877c6 100644 --- a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -9,22 +9,23 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +// 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:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_webview_pro/platform_interface.dart'; import 'package:flutter_webview_pro/webview_flutter.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter/platform_interface.dart'; +import 'package:webview_flutter/webview_flutter.dart'; Future main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - const bool _skipDueToIssue86757 = true; - final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0); server.forEach((HttpRequest request) { if (request.uri.path == '/hello.txt') { @@ -45,10 +46,10 @@ Future main() async { final String secondaryUrl = '$prefixUrl/secondary.txt'; final String headersUrl = '$prefixUrl/headers'; - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('initialUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); + final Completer pageFinishedCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -58,18 +59,22 @@ Future main() async { onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, + onPageFinished: pageFinishedCompleter.complete, ), ), ); + final WebViewController controller = await controllerCompleter.future; + await pageFinishedCompleter.future; + final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); - }, skip: _skipDueToIssue86757); + }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('loadUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); + final StreamController pageLoads = StreamController(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -79,14 +84,20 @@ Future main() async { onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, + onPageFinished: (String url) { + pageLoads.add(url); + }, ), ), ); final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl(secondaryUrl); - final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, secondaryUrl); - }, skip: _skipDueToIssue86757); + await expectLater( + pageLoads.stream.firstWhere((String url) => url == secondaryUrl), + completion(secondaryUrl), + ); + }); testWidgets('evaluateJavascript', (WidgetTester tester) async { final Completer controllerCompleter = @@ -110,7 +121,6 @@ Future main() async { expect(result, equals('2')); }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -149,15 +159,14 @@ Future main() async { final String content = await controller .runJavascriptReturningResult('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); - }, skip: Platform.isAndroid && _skipDueToIssue86757); + }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('JavascriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer pageStarted = Completer(); final Completer pageLoaded = Completer(); - final List messagesReceived = []; + final Completer channelCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -174,7 +183,7 @@ Future main() async { JavascriptChannel( name: 'Echo', onMessageReceived: (JavascriptMessage message) { - messagesReceived.add(message.message); + channelCompleter.complete(message.message); }, ), }, @@ -191,10 +200,11 @@ Future main() async { await pageStarted.future; await pageLoaded.future; - expect(messagesReceived, isEmpty); + expect(channelCompleter.isCompleted, isFalse); await controller.runJavascript('Echo.postMessage("hello");'); - expect(messagesReceived, equals(['hello'])); - }, skip: Platform.isAndroid && _skipDueToIssue86757); + + await expectLater(channelCompleter.future, completion('hello')); + }); testWidgets('resize webview', (WidgetTester tester) async { final Completer initialResizeCompleter = Completer(); @@ -228,12 +238,12 @@ Future main() async { testWidgets('set custom userAgent', (WidgetTester tester) async { final Completer controllerCompleter1 = Completer(); - final GlobalKey _globalKey = GlobalKey(); + final GlobalKey globalKey = GlobalKey(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( - key: _globalKey, + key: globalKey, initialUrl: 'about:blank', javascriptMode: JavascriptMode.unrestricted, userAgent: 'Custom_User_Agent1', @@ -251,7 +261,7 @@ Future main() async { Directionality( textDirection: TextDirection.ltr, child: WebView( - key: _globalKey, + key: globalKey, initialUrl: 'about:blank', javascriptMode: JavascriptMode.unrestricted, userAgent: 'Custom_User_Agent2', @@ -263,18 +273,17 @@ Future main() async { expect(customUserAgent2, 'Custom_User_Agent2'); }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('use default platform userAgent after webView is rebuilt', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); - final GlobalKey _globalKey = GlobalKey(); + final GlobalKey globalKey = GlobalKey(); // Build the webView with no user agent to get the default platform user agent. await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( - key: _globalKey, + key: globalKey, initialUrl: primaryUrl, javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController controller) { @@ -290,7 +299,7 @@ Future main() async { Directionality( textDirection: TextDirection.ltr, child: WebView( - key: _globalKey, + key: globalKey, initialUrl: 'about:blank', javascriptMode: JavascriptMode.unrestricted, userAgent: 'Custom_User_Agent', @@ -304,7 +313,7 @@ Future main() async { Directionality( textDirection: TextDirection.ltr, child: WebView( - key: _globalKey, + key: globalKey, initialUrl: 'about:blank', javascriptMode: JavascriptMode.unrestricted, ), @@ -313,7 +322,7 @@ Future main() async { final String customUserAgent2 = await _getUserAgent(controller); expect(customUserAgent2, defaultPlatformUserAgent); - }, skip: Platform.isAndroid && _skipDueToIssue86757); + }); group('Video playback policy', () { late String videoTestBase64; @@ -401,8 +410,6 @@ Future main() async { onPageFinished: (String url) { pageLoaded.complete(null); }, - initialMediaPlaybackPolicy: - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, ), ), ); @@ -460,8 +467,6 @@ Future main() async { onPageFinished: (String url) { pageLoaded.complete(null); }, - initialMediaPlaybackPolicy: - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, ), ), ); @@ -558,7 +563,6 @@ Future main() async { pageLoaded.complete(null); }, initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - allowsInlineMediaPlayback: false, ), ), ); @@ -663,8 +667,6 @@ Future main() async { onPageFinished: (String url) { pageLoaded.complete(null); }, - initialMediaPlaybackPolicy: - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, ), ), ); @@ -677,7 +679,7 @@ Future main() async { expect(isPaused, _webviewBool(true)); }); - testWidgets('Changes to initialMediaPlaybackPolocy are ignored', + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -732,8 +734,6 @@ Future main() async { onPageFinished: (String url) { pageLoaded.complete(null); }, - initialMediaPlaybackPolicy: - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, ), ), ); @@ -798,7 +798,6 @@ Future main() async { }); group('Programmatic Scroll', () { - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { const String scrollTestPage = ''' @@ -873,10 +872,10 @@ Future main() async { scrollPosY = await controller.getScrollY(); expect(scrollPosX, X_SCROLL * 2); expect(scrollPosY, Y_SCROLL * 2); - }, skip: Platform.isAndroid && _skipDueToIssue86757); + }); }); - // Minimial end-to-end testing of the legacy Android implementation. + // Minimal end-to-end testing of the legacy Android implementation. group('AndroidWebView (virtual display)', () { setUpAll(() { WebView.platform = AndroidWebView(); @@ -889,7 +888,7 @@ Future main() async { testWidgets('initialUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); - final Completer loadCompleter = Completer(); + final Completer pageFinishedCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -899,23 +898,23 @@ Future main() async { onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, - onPageFinished: (String url) { - loadCompleter.complete(); - }, + onPageFinished: pageFinishedCompleter.complete, ), ), ); + final WebViewController controller = await controllerCompleter.future; - await loadCompleter.future; + await pageFinishedCompleter.future; + final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); }); - }, skip: !Platform.isAndroid || _skipDueToIssue86757); + }, skip: !Platform.isAndroid); group('NavigationDelegate', () { const String blankPage = ''; - final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + - base64Encode(const Utf8Encoder().convert(blankPage)); + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + '${base64Encode(const Utf8Encoder().convert(blankPage))}'; testWidgets('can allow requests', (WidgetTester tester) async { final Completer controllerCompleter = @@ -1149,7 +1148,6 @@ Future main() async { expect(currentUrl, primaryUrl); }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('target _blank opens in same window', (WidgetTester tester) async { final Completer controllerCompleter = @@ -1175,9 +1173,8 @@ Future main() async { await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); - }, skip: Platform.isAndroid && _skipDueToIssue86757); + }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets( 'can open new window and go back', (WidgetTester tester) async { @@ -1213,9 +1210,61 @@ Future main() async { expect(controller.canGoBack(), completion(true)); await controller.goBack(); await pageLoaded.future; - expect(controller.currentUrl(), completion(primaryUrl)); + await expectLater(controller.currentUrl(), completion(primaryUrl)); + }, + ); + + testWidgets( + 'clearCache should clear local storage', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + + Completer pageLoadCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (_) => pageLoadCompleter.complete(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + + await pageLoadCompleter.future; + pageLoadCompleter = Completer(); + + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('localStorage.setItem("myCat", "Tom");'); + final String myCatItem = await controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', + ); + expect(myCatItem, _webviewString('Tom')); + + await controller.clearCache(); + await pageLoadCompleter.future; + + late final String? nullItem; + try { + nullItem = await controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', + ); + } catch (exception) { + if (defaultTargetPlatform == TargetPlatform.iOS && + exception is ArgumentError && + (exception.message as String).contains( + 'Result of JavaScript execution returned a `null` value.')) { + nullItem = ''; + } + } + expect(nullItem, _webviewNull()); }, - skip: _skipDueToIssue86757, ); } @@ -1228,6 +1277,24 @@ String _webviewBool(bool value) { return value ? 'true' : 'false'; } +// JavaScript `null` evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewNull() { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return ''; + } + return 'null'; +} + +// JavaScript String evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewString(String value) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return value; + } + return '"$value"'; +} + /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. Future _getUserAgent(WebViewController controller) async { return _runJavascriptReturningResult(controller, 'navigator.userAgent;'); @@ -1244,7 +1311,8 @@ Future _runJavascriptReturningResult( class ResizableWebView extends StatefulWidget { const ResizableWebView( - {required this.onResize, required this.onPageFinished}); + {Key? key, required this.onResize, required this.onPageFinished}) + : super(key: key); final JavascriptMessageHandler onResize; final VoidCallback onPageFinished; diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 3822dd54fe69..d93bb7b8ed4a 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -13,7 +13,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_webview_pro/webview_flutter.dart'; import 'package:path_provider/path_provider.dart'; -void main() => runApp(MaterialApp(home: WebViewExample())); +void main() => runApp(const MaterialApp(home: WebViewExample())); const String kNavigationExamplePage = ''' @@ -40,8 +40,8 @@ const String kLocalExamplePage = '''

Local demo page

- This is an example page used to demonstrate how to load a local file or HTML - string using the Flutter + This is an example page used to demonstrate how to load a local file or HTML + string using the Flutter webview plugin.

@@ -71,8 +71,12 @@ const String kTransparentBackgroundPage = ''' '''; class WebViewExample extends StatefulWidget { + const WebViewExample({Key? key, this.cookieManager}) : super(key: key); + + final CookieManager? cookieManager; + @override - _WebViewExampleState createState() => _WebViewExampleState(); + State createState() => _WebViewExampleState(); } class _WebViewExampleState extends State { @@ -96,44 +100,40 @@ class _WebViewExampleState extends State { // This drop down menu demonstrates that Flutter widgets can be shown over the web view. actions: [ NavigationControls(_controller.future), - SampleMenu(_controller.future), + SampleMenu(_controller.future, widget.cookieManager), ], ), - // We're using a Builder here so we have a context that is below the Scaffold - // to allow calling Scaffold.of(context) so we can show a snackbar. - body: Builder(builder: (BuildContext context) { - return WebView( - initialUrl: 'https://www.wjx.cn/jq/27265670.aspx', + body: WebView( + initialUrl: 'https://www.wjx.cn/jq/27265670.aspx', // initialUrl: 'https://flutter.dev', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController webViewController) { - _controller.complete(webViewController); - }, - onProgress: (int progress) { - print('WebView is loading (progress : $progress%)'); - }, - javascriptChannels: { - _toasterJavascriptChannel(context), - }, - navigationDelegate: (NavigationRequest request) { - if (request.url.startsWith('https://www.youtube.com/')) { - print('blocking navigation to $request}'); - return NavigationDecision.prevent; - } - print('allowing navigation to $request'); - return NavigationDecision.navigate; - }, - onPageStarted: (String url) { - print('Page started loading: $url'); - }, - onPageFinished: (String url) { - print('Page finished loading: $url'); - }, - gestureNavigationEnabled: true, - backgroundColor: const Color(0x00000000), + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + _controller.complete(webViewController); + }, + onProgress: (int progress) { + print('WebView is loading (progress : $progress%)'); + }, + javascriptChannels: { + _toasterJavascriptChannel(context), + }, + navigationDelegate: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $request}'); + return NavigationDecision.prevent; + } + print('allowing navigation to $request'); + return NavigationDecision.navigate; + }, + onPageStarted: (String url) { + print('Page started loading: $url'); + }, + onPageFinished: (String url) { + print('Page finished loading: $url'); + }, + gestureNavigationEnabled: true, + backgroundColor: const Color(0x00000000), geolocationEnabled: true, // set geolocationEnable true or not - ); - }), + ), floatingActionButton: favoriteButton(), ); } @@ -142,8 +142,7 @@ class _WebViewExampleState extends State { return JavascriptChannel( name: 'Toaster', onMessageReceived: (JavascriptMessage message) { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message.message)), ); }); @@ -154,19 +153,24 @@ class _WebViewExampleState extends State { future: _controller.future, builder: (BuildContext context, AsyncSnapshot controller) { - if (controller.hasData) { - return FloatingActionButton( - onPressed: () async { - final String url = (await controller.data!.currentUrl())!; - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( - SnackBar(content: Text('Favorited $url')), - ); - }, - child: const Icon(Icons.favorite), - ); - } - return Container(); + return FloatingActionButton( + onPressed: () async { + String? url; + if (controller.hasData) { + url = await controller.data!.currentUrl(); + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + controller.hasData + ? 'Favorited $url' + : 'Unable to favorite', + ), + ), + ); + }, + child: const Icon(Icons.favorite), + ); }); } } @@ -189,10 +193,12 @@ enum MenuOptions { } class SampleMenu extends StatelessWidget { - SampleMenu(this.controller); + SampleMenu(this.controller, CookieManager? cookieManager, {Key? key}) + : cookieManager = cookieManager ?? CookieManager(), + super(key: key); final Future controller; - final CookieManager cookieManager = CookieManager(); + late final CookieManager cookieManager; @override Widget build(BuildContext context) { @@ -251,8 +257,8 @@ class SampleMenu extends StatelessWidget { itemBuilder: (BuildContext context) => >[ PopupMenuItem( value: MenuOptions.showUserAgent, - child: const Text('Show user agent'), enabled: controller.hasData, + child: const Text('Show user agent'), ), const PopupMenuItem( value: MenuOptions.listCookies, @@ -325,8 +331,7 @@ class SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { final String cookies = await controller.runJavascriptReturningResult('document.cookie'); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Column( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, @@ -342,8 +347,7 @@ class SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { await controller.runJavascript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(const SnackBar( + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Added a test entry to cache.'), )); } @@ -351,6 +355,7 @@ class SampleMenu extends StatelessWidget { Future _onListCache( WebViewController controller, BuildContext context) async { await controller.runJavascript('caches.keys()' + // ignore: missing_whitespace_between_adjacent_strings '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Toaster.postMessage(caches))'); } @@ -358,8 +363,7 @@ class SampleMenu extends StatelessWidget { Future _onClearCache( WebViewController controller, BuildContext context) async { await controller.clearCache(); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(const SnackBar( + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Cache cleared.'), )); } @@ -370,8 +374,7 @@ class SampleMenu extends StatelessWidget { if (!hadCookies) { message = 'There are no cookies.'; } - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(message), )); } @@ -390,7 +393,7 @@ class SampleMenu extends StatelessWidget { Future _onSetCookie( WebViewController controller, BuildContext context) async { - await CookieManager().setCookie( + await cookieManager.setCookie( const WebViewCookie( name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), ); @@ -457,8 +460,9 @@ class SampleMenu extends StatelessWidget { } class NavigationControls extends StatelessWidget { - const NavigationControls(this._webViewControllerFuture) - : assert(_webViewControllerFuture != null); + const NavigationControls(this._webViewControllerFuture, {Key? key}) + : assert(_webViewControllerFuture != null), + super(key: key); final Future _webViewControllerFuture; @@ -481,8 +485,7 @@ class NavigationControls extends StatelessWidget { if (await controller!.canGoBack()) { await controller.goBack(); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No back history item')), ); return; @@ -497,8 +500,7 @@ class NavigationControls extends StatelessWidget { if (await controller!.canGoForward()) { await controller.goForward(); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('No forward history item')), ); diff --git a/packages/webview_flutter/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml index 364b5b784d79..c144f246e7ec 100644 --- a/packages/webview_flutter/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" dependencies: flutter: @@ -19,14 +19,13 @@ dependencies: path: ../ dev_dependencies: - espresso: ^0.1.0+2 + espresso: ^0.2.0 flutter_driver: sdk: flutter flutter_test: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/webview_flutter/webview_flutter/example/test/main_test.dart b/packages/webview_flutter/webview_flutter/example/test/main_test.dart new file mode 100644 index 000000000000..867633366e1a --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/test/main_test.dart @@ -0,0 +1,39 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_example/main.dart'; + +void main() { + testWidgets('Test snackbar from ScaffoldMessenger', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: WebViewExample(cookieManager: FakeCookieManager()), + ), + ); + expect(find.byIcon(Icons.favorite), findsOneWidget); + await tester.tap(find.byIcon(Icons.favorite)); + await tester.pump(); + expect(find.byType(SnackBar), findsOneWidget); + }); +} + +class FakeCookieManager implements CookieManager { + factory FakeCookieManager() { + return _instance ??= FakeCookieManager._(); + } + + FakeCookieManager._(); + + static FakeCookieManager? _instance; + + @override + Future clearCookies() => throw UnimplementedError(); + + @override + Future setCookie(WebViewCookie cookie) => throw UnimplementedError(); +} diff --git a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_controller.dart b/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_controller.dart new file mode 100644 index 000000000000..bd03b247027e --- /dev/null +++ b/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_controller.dart @@ -0,0 +1,260 @@ +// 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:math'; + +// 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:flutter/material.dart'; +import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; + +/// Controls a WebView provided by the host platform. +/// +/// Pass this to a [WebViewWidget] to display the WebView. +class WebViewController { + /// Constructs a [WebViewController]. + WebViewController() + : this.fromPlatformCreationParams( + const PlatformWebViewControllerCreationParams(), + ); + + /// Constructs a [WebViewController] from creation params for a specific + /// platform. + WebViewController.fromPlatformCreationParams( + PlatformWebViewControllerCreationParams params, + ) : this.fromPlatform(PlatformWebViewController(params)); + + /// Constructs a [WebViewController] from a specific platform implementation. + WebViewController.fromPlatform(this.platform); + + /// Implementation of [PlatformWebViewController] for the current platform. + final PlatformWebViewController platform; + + /// Loads the file located on the specified [absoluteFilePath]. + /// + /// The [absoluteFilePath] parameter should contain the absolute path to the + /// file as it is stored on the device. For example: + /// `/Users/username/Documents/www/index.html`. + /// + /// Throws a `PlatformException` if the [absoluteFilePath] does not exist. + Future loadFile(String absoluteFilePath) { + return platform.loadFile(absoluteFilePath); + } + + /// Loads the Flutter asset specified in the pubspec.yaml file. + /// + /// Throws a `PlatformException` if [key] is not part of the specified assets + /// in the pubspec.yaml file. + Future loadFlutterAsset(String key) { + assert(key.isNotEmpty); + return platform.loadFlutterAsset(key); + } + + /// Loads the supplied HTML string. + /// + /// The [baseUrl] parameter is used when resolving relative URLs within the + /// HTML string. + Future loadHtmlString(String html, {String? baseUrl}) { + assert(html.isNotEmpty); + return platform.loadHtmlString(html, baseUrl: baseUrl); + } + + /// Makes a specific HTTP request ands loads the response in the webview. + /// + /// [method] must be one of the supported HTTP methods in [LoadRequestMethod]. + /// + /// If [headers] is not empty, its key-value pairs will be added as the + /// headers for the request. + /// + /// If [body] is not null, it will be added as the body for the request. + /// + /// Throws an ArgumentError if [uri] has an empty scheme. + Future loadRequest( + Uri uri, { + LoadRequestMethod method = LoadRequestMethod.get, + Map headers = const {}, + Uint8List? body, + }) { + if (uri.scheme.isEmpty) { + throw ArgumentError('Missing scheme in uri: $uri'); + } + return platform.loadRequest(LoadRequestParams( + uri: uri, + method: method, + headers: headers, + body: body, + )); + } + + /// Returns the current URL that the WebView is displaying. + /// + /// If no URL was ever loaded, returns `null`. + Future currentUrl() { + return platform.currentUrl(); + } + + /// Checks whether there's a back history item. + Future canGoBack() { + return platform.canGoBack(); + } + + /// Checks whether there's a forward history item. + Future canGoForward() { + return platform.canGoForward(); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + return platform.goBack(); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + return platform.goForward(); + } + + /// Reloads the current URL. + Future reload() { + return platform.reload(); + } + + /// Clears all caches used by the WebView. + /// + /// The following caches are cleared: + /// 1. Browser HTTP Cache. + /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) + /// caches. Service workers tend to use this cache. + /// 3. Application cache. + Future clearCache() { + return platform.clearCache(); + } + + /// Clears the local storage used by the WebView. + Future clearLocalStorage() { + return platform.clearLocalStorage(); + } + + /// Runs the given JavaScript in the context of the current page. + /// + /// The Future completes with an error if a JavaScript error occurred. + Future runJavaScript(String javaScript) { + return platform.runJavaScript(javaScript); + } + + /// Runs the given JavaScript in the context of the current page, and returns + /// the result. + /// + /// The Future completes with an error if a JavaScript error occurred, or if + /// the type the given expression evaluates to is unsupported. Unsupported + /// values include certain non-primitive types on iOS, as well as `undefined` + /// or `null` on iOS 14+. + Future runJavaScriptReturningResult(String javaScript) { + return platform.runJavaScriptReturningResult(javaScript); + } + + /// Adds a new JavaScript channel to the set of enabled channels. + /// + /// The JavaScript code can then call `postMessage` on that object to send a + /// message that will be passed to [onMessageReceived]. + /// + /// For example, after adding the following JavaScript channel: + /// + /// ```dart + /// final WebViewController controller = WebViewController(); + /// controller.addJavaScriptChannel( + /// name: 'Print', + /// onMessageReceived: (JavascriptMessage message) { + /// print(message.message); + /// }, + /// ); + /// ``` + /// + /// JavaScript code can call: + /// + /// ```javascript + /// Print.postMessage('Hello'); + /// ``` + /// + /// to asynchronously invoke the message handler which will print the message + /// to standard output. + /// + /// Adding a new JavaScript channel only takes affect after the next page is + /// loaded. + /// + /// A channel [name] cannot be the same for multiple channels. + Future addJavaScriptChannel( + String name, { + required void Function(JavaScriptMessage) onMessageReceived, + }) { + assert(name.isNotEmpty); + return platform.addJavaScriptChannel(JavaScriptChannelParams( + name: name, + onMessageReceived: onMessageReceived, + )); + } + + /// Removes the JavaScript channel with the matching name from the set of + /// enabled channels. + /// + /// This disables the channel with the matching name if it was previously + /// enabled through the [addJavaScriptChannel]. + Future removeJavaScriptChannel(String javaScriptChannelName) { + return platform.removeJavaScriptChannel(javaScriptChannelName); + } + + /// The title of the currently loaded page. + Future getTitle() { + return platform.getTitle(); + } + + /// Sets the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the position to scroll to in WebView + /// pixels. + Future scrollTo(int x, int y) { + return platform.scrollTo(x, y); + } + + /// Moves the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll + /// by. + Future scrollBy(int x, int y) { + return platform.scrollBy(x, y); + } + + /// Returns the current scroll position of this view. + /// + /// Scroll position is measured from the top left. + Future getScrollPosition() async { + final Point position = await platform.getScrollPosition(); + return Offset(position.x.toDouble(), position.y.toDouble()); + } + + /// Whether to support zooming using the on-screen zoom controls and gestures. + Future enableZoom(bool enabled) { + return platform.enableZoom(enabled); + } + + /// Sets the current background color of this view. + Future setBackgroundColor(Color color) { + return platform.setBackgroundColor(color); + } + + /// Sets the JavaScript execution mode to be used by the WebView. + Future setJavaScriptMode(JavaScriptMode javaScriptMode) { + return platform.setJavaScriptMode(javaScriptMode); + } + + /// Sets the value used for the HTTP `User-Agent:` request header. + Future setUserAgent(String? userAgent) { + return platform.setUserAgent(userAgent); + } +} diff --git a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_cookie_manager.dart new file mode 100644 index 000000000000..a1091fa3c7b1 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_cookie_manager.dart @@ -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 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; + +/// Manages cookies pertaining to all WebViews. +class WebViewCookieManager { + /// Constructs a [WebViewCookieManager]. + WebViewCookieManager() + : this.fromPlatformCreationParams( + const PlatformWebViewCookieManagerCreationParams(), + ); + + /// Constructs a [WebViewCookieManager] from creation params for a specific + /// platform. + WebViewCookieManager.fromPlatformCreationParams( + PlatformWebViewCookieManagerCreationParams params, + ) : this.fromPlatform(PlatformWebViewCookieManager(params)); + + /// Constructs a [WebViewCookieManager] from a specific platform + /// implementation. + WebViewCookieManager.fromPlatform(this.platform); + + /// Implementation of [PlatformWebViewCookieManager] for the current platform. + final PlatformWebViewCookieManager platform; + + /// Clears all cookies for all WebViews. + /// + /// Returns true if cookies were present before clearing, else false. + Future clearCookies() => platform.clearCookies(); + + /// Sets a cookie for all WebView instances. + /// + /// This is a no op on iOS versions below 11. + Future setCookie(WebViewCookie cookie) => platform.setCookie(cookie); +} diff --git a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_widget.dart b/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_widget.dart new file mode 100644 index 000000000000..06e4f78028df --- /dev/null +++ b/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_widget.dart @@ -0,0 +1,64 @@ +// 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/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; + +import 'webview_controller.dart'; + +/// Displays a native WebView as a Widget. +class WebViewWidget extends StatelessWidget { + /// Constructs a [WebViewWidget]. + WebViewWidget({ + Key? key, + required WebViewController controller, + TextDirection layoutDirection = TextDirection.ltr, + Set> gestureRecognizers = + const >{}, + }) : this.fromPlatformCreationParams( + key: key, + params: PlatformWebViewWidgetCreationParams( + controller: controller.platform, + layoutDirection: layoutDirection, + gestureRecognizers: gestureRecognizers, + ), + ); + + /// Constructs a [WebViewWidget] from creation params for a specific + /// platform. + WebViewWidget.fromPlatformCreationParams({ + Key? key, + required PlatformWebViewWidgetCreationParams params, + }) : this.fromPlatform(key: key, platform: PlatformWebViewWidget(params)); + + /// Constructs a [WebViewWidget] from a specific platform implementation. + WebViewWidget.fromPlatform({Key? key, required this.platform}) + : super(key: key); + + /// Implementation of [PlatformWebViewWidget] for the current platform. + final PlatformWebViewWidget platform; + + /// The layout direction to use for the embedded WebView. + late final TextDirection layoutDirection = platform.params.layoutDirection; + + /// Specifies which gestures should be consumed by the web view. + /// + /// It is possible for other gesture recognizers to be competing with the web + /// view on pointer events, e.g if the web view is inside a [ListView] the + /// [ListView] will want to handle vertical drags. The web view will claim + /// gestures that are recognized by any of the recognizers on this list. + /// + /// When `gestureRecognizers` is empty (default), the web view will only + /// handle pointer events for gestures that were not claimed by any other + /// gesture recognizer. + late final Set> gestureRecognizers = + platform.params.gestureRecognizers; + + @override + Widget build(BuildContext context) { + return platform.build(context); + } +} diff --git a/packages/webview_flutter/webview_flutter/lib/src/v4/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/src/v4/webview_flutter.dart new file mode 100644 index 000000000000..f4a0b207e27a --- /dev/null +++ b/packages/webview_flutter/webview_flutter/lib/src/v4/webview_flutter.dart @@ -0,0 +1,12 @@ +// 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 webview_flutter; + +export 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart' + show JavaScriptMessage, LoadRequestMethod, WebViewCookie; + +export 'src/webview_controller.dart'; +export 'src/webview_cookie_manager.dart'; +export 'src/webview_widget.dart'; diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart index 0fd59075f83a..048e214ca577 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -7,15 +7,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:webview_pro_android/webview_android_cookie_manager.dart'; import 'package:webview_pro_android/webview_surface_android.dart'; import 'package:webview_pro_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_pro_wkwebview/webview_flutter_wkwebview.dart'; -import '../platform_interface.dart'; - /// Optional callback invoked when a web view is first created. [controller] is /// the [WebViewController] for the created web view. typedef WebViewCreatedCallback = void Function(WebViewController controller); diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 9fd8c08e1a97..6b271040c43a 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -2,25 +2,25 @@ name: flutter_webview_pro description: A Flutter plugin that provides a WebView widget who Support photo upload/take camera and Geolocation. repository: https://github.com/wenzhiming/flutter-plugins/tree/dev/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/wenzhiming/flutter-plugins/issues -version: 3.0.1+4 +version: 3.0.4+1 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: platforms: android: - default_package: webview_flutter_android + default_package: webview_pro_android ios: - default_package: webview_flutter_wkwebview + default_package: webview_pro_wkwebview dependencies: flutter: sdk: flutter webview_pro_android: ^2.8.3+3 - webview_pro_platform_interface: ^1.8.1+2 + webview_pro_platform_interface: ^1.8.1+2 #^1.9.3+1 webview_pro_wkwebview: ^2.7.1+3 dev_dependencies: @@ -30,4 +30,3 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.16 - pedantic: ^1.10.0 diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.dart b/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.dart new file mode 100644 index 000000000000..f767a2e48d5e --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.dart @@ -0,0 +1,353 @@ +// 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:math'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter/src/v4/src/webview_controller.dart'; +import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; + +import 'webview_controller_test.mocks.dart'; + +@GenerateMocks([PlatformWebViewController]) +void main() { + test('loadFile', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.loadFile('file/path'); + verify(mockPlatformWebViewController.loadFile('file/path')); + }); + + test('loadFlutterAsset', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.loadFlutterAsset('file/path'); + verify(mockPlatformWebViewController.loadFlutterAsset('file/path')); + }); + + test('loadHtmlString', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.loadHtmlString('html', baseUrl: 'baseUrl'); + verify(mockPlatformWebViewController.loadHtmlString( + 'html', + baseUrl: 'baseUrl', + )); + }); + + test('loadRequest', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.loadRequest( + Uri(scheme: 'https', host: 'dart.dev'), + method: LoadRequestMethod.post, + headers: {'a': 'header'}, + body: Uint8List(0), + ); + + final LoadRequestParams params = + verify(mockPlatformWebViewController.loadRequest(captureAny)) + .captured[0] as LoadRequestParams; + expect(params.uri, Uri(scheme: 'https', host: 'dart.dev')); + expect(params.method, LoadRequestMethod.post); + expect(params.headers, {'a': 'header'}); + expect(params.body, Uint8List(0)); + }); + + test('currentUrl', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + when(mockPlatformWebViewController.currentUrl()).thenAnswer( + (_) => Future.value('https://dart.dev'), + ); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await expectLater( + webViewController.currentUrl(), + completion('https://dart.dev'), + ); + }); + + test('canGoBack', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + when(mockPlatformWebViewController.canGoBack()).thenAnswer( + (_) => Future.value(false), + ); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await expectLater(webViewController.canGoBack(), completion(false)); + }); + + test('canGoForward', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + when(mockPlatformWebViewController.canGoForward()).thenAnswer( + (_) => Future.value(true), + ); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await expectLater(webViewController.canGoForward(), completion(true)); + }); + + test('goBack', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.goBack(); + verify(mockPlatformWebViewController.goBack()); + }); + + test('goForward', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.goForward(); + verify(mockPlatformWebViewController.goForward()); + }); + + test('reload', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.reload(); + verify(mockPlatformWebViewController.reload()); + }); + + test('clearCache', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.clearCache(); + verify(mockPlatformWebViewController.clearCache()); + }); + + test('clearLocalStorage', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.clearLocalStorage(); + verify(mockPlatformWebViewController.clearLocalStorage()); + }); + + test('runJavaScript', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.runJavaScript('1 + 1'); + verify(mockPlatformWebViewController.runJavaScript('1 + 1')); + }); + + test('runJavaScriptReturningResult', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + when(mockPlatformWebViewController.runJavaScriptReturningResult('1 + 1')) + .thenAnswer((_) => Future.value('2')); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await expectLater( + webViewController.runJavaScriptReturningResult('1 + 1'), + completion('2'), + ); + }); + + test('addJavaScriptChannel', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + void onMessageReceived(JavaScriptMessage message) {} + await webViewController.addJavaScriptChannel( + 'name', + onMessageReceived: onMessageReceived, + ); + + final JavaScriptChannelParams params = + verify(mockPlatformWebViewController.addJavaScriptChannel(captureAny)) + .captured[0] as JavaScriptChannelParams; + expect(params.name, 'name'); + expect(params.onMessageReceived, onMessageReceived); + }); + + test('removeJavaScriptChannel', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.removeJavaScriptChannel('channel'); + verify(mockPlatformWebViewController.removeJavaScriptChannel('channel')); + }); + + test('getTitle', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + when(mockPlatformWebViewController.getTitle()) + .thenAnswer((_) => Future.value('myTitle')); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await expectLater(webViewController.getTitle(), completion('myTitle')); + }); + + test('scrollTo', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.scrollTo(2, 3); + verify(mockPlatformWebViewController.scrollTo(2, 3)); + }); + + test('scrollBy', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.scrollBy(2, 3); + verify(mockPlatformWebViewController.scrollBy(2, 3)); + }); + + test('getScrollPosition', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + when(mockPlatformWebViewController.getScrollPosition()).thenAnswer( + (_) => Future>.value( + const Point(2, 3), + ), + ); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await expectLater( + webViewController.getScrollPosition(), + completion(const Offset(2.0, 3.0)), + ); + }); + + test('enableZoom', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.enableZoom(false); + verify(mockPlatformWebViewController.enableZoom(false)); + }); + + test('setBackgroundColor', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.setBackgroundColor(Colors.green); + verify(mockPlatformWebViewController.setBackgroundColor(Colors.green)); + }); + + test('setJavaScriptMode', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.setJavaScriptMode(JavaScriptMode.disabled); + verify( + mockPlatformWebViewController.setJavaScriptMode(JavaScriptMode.disabled), + ); + }); + + test('setUserAgent', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + await webViewController.setUserAgent('userAgent'); + verify(mockPlatformWebViewController.setUserAgent('userAgent')); + }); +} diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.mocks.dart new file mode 100644 index 000000000000..f0fb4b47fc6e --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.mocks.dart @@ -0,0 +1,203 @@ +// Mocks generated by Mockito 5.3.0 from annotations +// in webview_flutter/test/v4/webview_controller_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; +import 'dart:math' as _i3; +import 'dart:ui' as _i7; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart' + as _i6; +import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart' + as _i4; +import 'package:webview_flutter_platform_interface/v4/src/webview_platform.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 + +class _FakePlatformWebViewControllerCreationParams_0 extends _i1.SmartFake + implements _i2.PlatformWebViewControllerCreationParams { + _FakePlatformWebViewControllerCreationParams_0( + Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakePoint_1 extends _i1.SmartFake + implements _i3.Point { + _FakePoint_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [PlatformWebViewController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlatformWebViewController extends _i1.Mock + implements _i4.PlatformWebViewController { + MockPlatformWebViewController() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.PlatformWebViewControllerCreationParams get params => + (super.noSuchMethod(Invocation.getter(#params), + returnValue: _FakePlatformWebViewControllerCreationParams_0( + this, Invocation.getter(#params))) + as _i2.PlatformWebViewControllerCreationParams); + @override + _i5.Future loadFile(String? absoluteFilePath) => (super.noSuchMethod( + Invocation.method(#loadFile, [absoluteFilePath]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); + @override + _i5.Future loadFlutterAsset(String? key) => (super.noSuchMethod( + Invocation.method(#loadFlutterAsset, [key]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); + @override + _i5.Future loadHtmlString(String? html, {String? baseUrl}) => + (super.noSuchMethod( + Invocation.method(#loadHtmlString, [html], {#baseUrl: baseUrl}), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) + as _i5.Future); + @override + _i5.Future loadRequest(_i2.LoadRequestParams? params) => + (super.noSuchMethod(Invocation.method(#loadRequest, [params]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) + as _i5.Future); + @override + _i5.Future currentUrl() => + (super.noSuchMethod(Invocation.method(#currentUrl, []), + returnValue: _i5.Future.value()) as _i5.Future); + @override + _i5.Future canGoBack() => + (super.noSuchMethod(Invocation.method(#canGoBack, []), + returnValue: _i5.Future.value(false)) as _i5.Future); + @override + _i5.Future canGoForward() => + (super.noSuchMethod(Invocation.method(#canGoForward, []), + returnValue: _i5.Future.value(false)) as _i5.Future); + @override + _i5.Future goBack() => (super.noSuchMethod( + Invocation.method(#goBack, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); + @override + _i5.Future goForward() => (super.noSuchMethod( + Invocation.method(#goForward, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); + @override + _i5.Future reload() => (super.noSuchMethod( + Invocation.method(#reload, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); + @override + _i5.Future clearCache() => (super.noSuchMethod( + Invocation.method(#clearCache, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); + @override + _i5.Future clearLocalStorage() => (super.noSuchMethod( + Invocation.method(#clearLocalStorage, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); + @override + _i5.Future setPlatformNavigationDelegate( + _i6.PlatformNavigationDelegate? handler) => + (super.noSuchMethod( + Invocation.method(#setPlatformNavigationDelegate, [handler]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) + as _i5.Future); + @override + _i5.Future runJavaScript(String? javaScript) => (super.noSuchMethod( + Invocation.method(#runJavaScript, [javaScript]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); + @override + _i5.Future runJavaScriptReturningResult(String? javaScript) => + (super.noSuchMethod( + Invocation.method(#runJavaScriptReturningResult, [javaScript]), + returnValue: _i5.Future.value('')) as _i5.Future); + @override + _i5.Future addJavaScriptChannel( + _i4.JavaScriptChannelParams? javaScriptChannelParams) => + (super.noSuchMethod( + Invocation.method(#addJavaScriptChannel, [javaScriptChannelParams]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: + _i5.Future.value()) as _i5.Future); + @override + _i5.Future removeJavaScriptChannel(String? javaScriptChannelName) => + (super.noSuchMethod( + Invocation.method(#removeJavaScriptChannel, [javaScriptChannelName]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: + _i5.Future.value()) as _i5.Future); + @override + _i5.Future getTitle() => + (super.noSuchMethod(Invocation.method(#getTitle, []), + returnValue: _i5.Future.value()) as _i5.Future); + @override + _i5.Future scrollTo(int? x, int? y) => (super.noSuchMethod( + Invocation.method(#scrollTo, [x, y]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); + @override + _i5.Future scrollBy(int? x, int? y) => (super.noSuchMethod( + Invocation.method(#scrollBy, [x, y]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); + @override + _i5.Future<_i3.Point> getScrollPosition() => + (super.noSuchMethod(Invocation.method(#getScrollPosition, []), + returnValue: _i5.Future<_i3.Point>.value(_FakePoint_1( + this, Invocation.method(#getScrollPosition, [])))) + as _i5.Future<_i3.Point>); + @override + _i5.Future enableDebugging(bool? enabled) => (super.noSuchMethod( + Invocation.method(#enableDebugging, [enabled]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); + @override + _i5.Future enableGestureNavigation(bool? enabled) => (super + .noSuchMethod(Invocation.method(#enableGestureNavigation, [enabled]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) + as _i5.Future); + @override + _i5.Future enableZoom(bool? enabled) => (super.noSuchMethod( + Invocation.method(#enableZoom, [enabled]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); + @override + _i5.Future setBackgroundColor(_i7.Color? color) => (super.noSuchMethod( + Invocation.method(#setBackgroundColor, [color]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); + @override + _i5.Future setJavaScriptMode(_i2.JavaScriptMode? javaScriptMode) => + (super.noSuchMethod( + Invocation.method(#setJavaScriptMode, [javaScriptMode]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) + as _i5.Future); + @override + _i5.Future setUserAgent(String? userAgent) => (super.noSuchMethod( + Invocation.method(#setUserAgent, [userAgent]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value()) as _i5.Future); +} diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.dart new file mode 100644 index 000000000000..e8152407fb92 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.dart @@ -0,0 +1,54 @@ +// 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:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter/src/v4/src/webview_cookie_manager.dart'; +import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; + +import 'webview_cookie_manager_test.mocks.dart'; + +@GenerateMocks([PlatformWebViewCookieManager]) +void main() { + group('WebViewCookieManager', () { + test('clearCookies', () async { + final MockPlatformWebViewCookieManager mockPlatformWebViewCookieManager = + MockPlatformWebViewCookieManager(); + when(mockPlatformWebViewCookieManager.clearCookies()).thenAnswer( + (_) => Future.value(false), + ); + + final WebViewCookieManager cookieManager = + WebViewCookieManager.fromPlatform( + mockPlatformWebViewCookieManager, + ); + + await expectLater(cookieManager.clearCookies(), completion(false)); + }); + + test('setCookie', () async { + final MockPlatformWebViewCookieManager mockPlatformWebViewCookieManager = + MockPlatformWebViewCookieManager(); + + final WebViewCookieManager cookieManager = + WebViewCookieManager.fromPlatform( + mockPlatformWebViewCookieManager, + ); + + const WebViewCookie cookie = WebViewCookie( + name: 'name', + value: 'value', + domain: 'domain', + ); + + await cookieManager.setCookie(cookie); + + final WebViewCookie capturedCookie = verify( + mockPlatformWebViewCookieManager.setCookie(captureAny), + ).captured.single as WebViewCookie; + expect(capturedCookie, cookie); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.mocks.dart new file mode 100644 index 000000000000..4bca8b6a1f12 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.mocks.dart @@ -0,0 +1,56 @@ +// Mocks generated by Mockito 5.3.0 from annotations +// in webview_flutter/test/v4/webview_cookie_manager_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/v4/src/platform_webview_cookie_manager.dart' + as _i3; +import 'package:webview_flutter_platform_interface/v4/src/webview_platform.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 + +class _FakePlatformWebViewCookieManagerCreationParams_0 extends _i1.SmartFake + implements _i2.PlatformWebViewCookieManagerCreationParams { + _FakePlatformWebViewCookieManagerCreationParams_0( + Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [PlatformWebViewCookieManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlatformWebViewCookieManager extends _i1.Mock + implements _i3.PlatformWebViewCookieManager { + MockPlatformWebViewCookieManager() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.PlatformWebViewCookieManagerCreationParams get params => + (super.noSuchMethod(Invocation.getter(#params), + returnValue: _FakePlatformWebViewCookieManagerCreationParams_0( + this, Invocation.getter(#params))) + as _i2.PlatformWebViewCookieManagerCreationParams); + @override + _i4.Future clearCookies() => + (super.noSuchMethod(Invocation.method(#clearCookies, []), + returnValue: _i4.Future.value(false)) as _i4.Future); + @override + _i4.Future setCookie(_i2.WebViewCookie? cookie) => (super.noSuchMethod( + Invocation.method(#setCookie, [cookie]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value()) as _i4.Future); +} diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.dart b/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.dart new file mode 100644 index 000000000000..455d8b371ec7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.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:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter/src/v4/webview_flutter.dart'; +import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; + +import 'webview_widget_test.mocks.dart'; + +@GenerateMocks([PlatformWebViewController, PlatformWebViewWidget]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('WebViewWidget', () { + testWidgets('build', (WidgetTester tester) async { + final MockPlatformWebViewWidget mockPlatformWebViewWidget = + MockPlatformWebViewWidget(); + when(mockPlatformWebViewWidget.build(any)).thenReturn(Container()); + + await tester.pumpWidget(WebViewWidget.fromPlatform( + platform: mockPlatformWebViewWidget, + )); + + expect(find.byType(Container), findsOneWidget); + }); + + testWidgets( + 'constructor parameters are correctly passed to creation params', + (WidgetTester tester) async { + WebViewPlatform.instance = TestWebViewPlatform(); + + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + final WebViewController webViewController = + WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + final WebViewWidget webViewWidget = WebViewWidget( + key: GlobalKey(), + controller: webViewController, + layoutDirection: TextDirection.rtl, + gestureRecognizers: >{ + Factory(() => EagerGestureRecognizer()), + }, + ); + + // The key passed to the default constructor is used by the super class + // and not passed to the platform implementation. + expect(webViewWidget.platform.params.key, isNull); + expect( + webViewWidget.platform.params.controller, + webViewController.platform, + ); + expect(webViewWidget.platform.params.layoutDirection, TextDirection.rtl); + expect( + webViewWidget.platform.params.gestureRecognizers.isNotEmpty, + isTrue, + ); + }); + }); +} + +class TestWebViewPlatform extends WebViewPlatform { + @override + PlatformWebViewWidget createPlatformWebViewWidget( + PlatformWebViewWidgetCreationParams params, + ) { + return TestPlatformWebViewWidget(params); + } +} + +class TestPlatformWebViewWidget extends PlatformWebViewWidget { + TestPlatformWebViewWidget(PlatformWebViewWidgetCreationParams params) + : super.implementation(params); + + @override + Widget build(BuildContext context) { + throw UnimplementedError(); + } +} diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.mocks.dart new file mode 100644 index 000000000000..e481d752be5d --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.mocks.dart @@ -0,0 +1,246 @@ +// Mocks generated by Mockito 5.3.0 from annotations +// in webview_flutter/test/v4/webview_widget_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i7; +import 'dart:math' as _i3; +import 'dart:ui' as _i9; + +import 'package:flutter/foundation.dart' as _i5; +import 'package:flutter/widgets.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart' + as _i8; +import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart' + as _i6; +import 'package:webview_flutter_platform_interface/v4/src/platform_webview_widget.dart' + as _i10; +import 'package:webview_flutter_platform_interface/v4/src/webview_platform.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 + +class _FakePlatformWebViewControllerCreationParams_0 extends _i1.SmartFake + implements _i2.PlatformWebViewControllerCreationParams { + _FakePlatformWebViewControllerCreationParams_0( + Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakePoint_1 extends _i1.SmartFake + implements _i3.Point { + _FakePoint_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakePlatformWebViewWidgetCreationParams_2 extends _i1.SmartFake + implements _i2.PlatformWebViewWidgetCreationParams { + _FakePlatformWebViewWidgetCreationParams_2( + Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeWidget_3 extends _i1.SmartFake implements _i4.Widget { + _FakeWidget_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [PlatformWebViewController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlatformWebViewController extends _i1.Mock + implements _i6.PlatformWebViewController { + MockPlatformWebViewController() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.PlatformWebViewControllerCreationParams get params => + (super.noSuchMethod(Invocation.getter(#params), + returnValue: _FakePlatformWebViewControllerCreationParams_0( + this, Invocation.getter(#params))) + as _i2.PlatformWebViewControllerCreationParams); + @override + _i7.Future loadFile(String? absoluteFilePath) => (super.noSuchMethod( + Invocation.method(#loadFile, [absoluteFilePath]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); + @override + _i7.Future loadFlutterAsset(String? key) => (super.noSuchMethod( + Invocation.method(#loadFlutterAsset, [key]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); + @override + _i7.Future loadHtmlString(String? html, {String? baseUrl}) => + (super.noSuchMethod( + Invocation.method(#loadHtmlString, [html], {#baseUrl: baseUrl}), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) + as _i7.Future); + @override + _i7.Future loadRequest(_i2.LoadRequestParams? params) => + (super.noSuchMethod(Invocation.method(#loadRequest, [params]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) + as _i7.Future); + @override + _i7.Future currentUrl() => + (super.noSuchMethod(Invocation.method(#currentUrl, []), + returnValue: _i7.Future.value()) as _i7.Future); + @override + _i7.Future canGoBack() => + (super.noSuchMethod(Invocation.method(#canGoBack, []), + returnValue: _i7.Future.value(false)) as _i7.Future); + @override + _i7.Future canGoForward() => + (super.noSuchMethod(Invocation.method(#canGoForward, []), + returnValue: _i7.Future.value(false)) as _i7.Future); + @override + _i7.Future goBack() => (super.noSuchMethod( + Invocation.method(#goBack, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); + @override + _i7.Future goForward() => (super.noSuchMethod( + Invocation.method(#goForward, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); + @override + _i7.Future reload() => (super.noSuchMethod( + Invocation.method(#reload, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); + @override + _i7.Future clearCache() => (super.noSuchMethod( + Invocation.method(#clearCache, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); + @override + _i7.Future clearLocalStorage() => (super.noSuchMethod( + Invocation.method(#clearLocalStorage, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); + @override + _i7.Future setPlatformNavigationDelegate( + _i8.PlatformNavigationDelegate? handler) => + (super.noSuchMethod( + Invocation.method(#setPlatformNavigationDelegate, [handler]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) + as _i7.Future); + @override + _i7.Future runJavaScript(String? javaScript) => (super.noSuchMethod( + Invocation.method(#runJavaScript, [javaScript]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); + @override + _i7.Future runJavaScriptReturningResult(String? javaScript) => + (super.noSuchMethod( + Invocation.method(#runJavaScriptReturningResult, [javaScript]), + returnValue: _i7.Future.value('')) as _i7.Future); + @override + _i7.Future addJavaScriptChannel( + _i6.JavaScriptChannelParams? javaScriptChannelParams) => + (super.noSuchMethod( + Invocation.method(#addJavaScriptChannel, [javaScriptChannelParams]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: + _i7.Future.value()) as _i7.Future); + @override + _i7.Future removeJavaScriptChannel(String? javaScriptChannelName) => + (super.noSuchMethod( + Invocation.method(#removeJavaScriptChannel, [javaScriptChannelName]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: + _i7.Future.value()) as _i7.Future); + @override + _i7.Future getTitle() => + (super.noSuchMethod(Invocation.method(#getTitle, []), + returnValue: _i7.Future.value()) as _i7.Future); + @override + _i7.Future scrollTo(int? x, int? y) => (super.noSuchMethod( + Invocation.method(#scrollTo, [x, y]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); + @override + _i7.Future scrollBy(int? x, int? y) => (super.noSuchMethod( + Invocation.method(#scrollBy, [x, y]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); + @override + _i7.Future<_i3.Point> getScrollPosition() => + (super.noSuchMethod(Invocation.method(#getScrollPosition, []), + returnValue: _i7.Future<_i3.Point>.value(_FakePoint_1( + this, Invocation.method(#getScrollPosition, [])))) + as _i7.Future<_i3.Point>); + @override + _i7.Future enableDebugging(bool? enabled) => (super.noSuchMethod( + Invocation.method(#enableDebugging, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); + @override + _i7.Future enableGestureNavigation(bool? enabled) => (super + .noSuchMethod(Invocation.method(#enableGestureNavigation, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) + as _i7.Future); + @override + _i7.Future enableZoom(bool? enabled) => (super.noSuchMethod( + Invocation.method(#enableZoom, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); + @override + _i7.Future setBackgroundColor(_i9.Color? color) => (super.noSuchMethod( + Invocation.method(#setBackgroundColor, [color]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); + @override + _i7.Future setJavaScriptMode(_i2.JavaScriptMode? javaScriptMode) => + (super.noSuchMethod( + Invocation.method(#setJavaScriptMode, [javaScriptMode]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) + as _i7.Future); + @override + _i7.Future setUserAgent(String? userAgent) => (super.noSuchMethod( + Invocation.method(#setUserAgent, [userAgent]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value()) as _i7.Future); +} + +/// A class which mocks [PlatformWebViewWidget]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlatformWebViewWidget extends _i1.Mock + implements _i10.PlatformWebViewWidget { + MockPlatformWebViewWidget() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.PlatformWebViewWidgetCreationParams get params => + (super.noSuchMethod(Invocation.getter(#params), + returnValue: _FakePlatformWebViewWidgetCreationParams_2( + this, Invocation.getter(#params))) + as _i2.PlatformWebViewWidgetCreationParams); + @override + _i4.Widget build(_i4.BuildContext? context) => + (super.noSuchMethod(Invocation.method(#build, [context]), + returnValue: + _FakeWidget_3(this, Invocation.method(#build, [context]))) + as _i4.Widget); +} diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart index e2f4cc09e364..07c9d86c13ca 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -85,9 +85,7 @@ void main() { JavascriptMode.unrestricted, ); - await tester.pumpWidget(const WebView( - javascriptMode: JavascriptMode.disabled, - )); + await tester.pumpWidget(const WebView()); final CreationParams disabledparams = captureBuildArgs( mockWebViewPlatform, @@ -455,7 +453,6 @@ void main() { await tester.pumpWidget( WebView( initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.disabled, onWebViewCreated: (WebViewController webViewController) { controller = webViewController; }, @@ -489,7 +486,6 @@ void main() { await tester.pumpWidget( WebView( initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.disabled, onWebViewCreated: (WebViewController webViewController) { controller = webViewController; }, @@ -527,7 +523,6 @@ void main() { await tester.pumpWidget( WebView( initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.disabled, onWebViewCreated: (WebViewController webViewController) { controller = webViewController; }, @@ -588,7 +583,7 @@ void main() { }); test('Only valid JavaScript channel names are allowed', () { - final JavascriptMessageHandler noOp = (JavascriptMessage msg) {}; + void noOp(JavascriptMessage msg) {} JavascriptChannel(name: 'Tts1', onMessageReceived: noOp); JavascriptChannel(name: '_Alarm', onMessageReceived: noOp); JavascriptChannel(name: 'foo_bar_', onMessageReceived: noOp); @@ -757,7 +752,6 @@ void main() { testWidgets('onPageStarted is null', (WidgetTester tester) async { await tester.pumpWidget(const WebView( initialUrl: 'https://youtube.com', - onPageStarted: null, )); final WebViewPlatformCallbacksHandler handler = captureBuildArgs( @@ -818,7 +812,6 @@ void main() { testWidgets('onPageFinished is null', (WidgetTester tester) async { await tester.pumpWidget(const WebView( initialUrl: 'https://youtube.com', - onPageFinished: null, )); final WebViewPlatformCallbacksHandler handler = captureBuildArgs( @@ -878,7 +871,6 @@ void main() { testWidgets('onLoadingProgress is null', (WidgetTester tester) async { await tester.pumpWidget(const WebView( initialUrl: 'https://youtube.com', - onProgress: null, )); final WebViewPlatformCallbacksHandler handler = captureBuildArgs( @@ -1033,7 +1025,6 @@ void main() { await tester.pumpWidget(WebView( key: key, - debuggingEnabled: false, )); final WebSettings disabledSettings = @@ -1046,9 +1037,7 @@ void main() { group('zoomEnabled', () { testWidgets('Enable zoom', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - zoomEnabled: true, - )); + await tester.pumpWidget(const WebView()); final CreationParams params = captureBuildArgs( mockWebViewPlatform, @@ -1075,7 +1064,6 @@ void main() { await tester.pumpWidget(WebView( key: key, - zoomEnabled: true, )); final WebSettings enabledSettings = diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart index 72d43d9d7b94..1288e930f9f2 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart @@ -1,11 +1,8 @@ -// 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. - -// Mocks generated by Mockito 5.0.16 from annotations +// Mocks generated by Mockito 5.3.0 from annotations // in webview_flutter/test/webview_flutter_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i9; import 'package:flutter/foundation.dart' as _i3; @@ -22,6 +19,7 @@ import 'package:webview_pro_platform_interface/src/platform_interface/webview_pl as _i10; import 'package:webview_pro_platform_interface/src/types/types.dart' as _i5; +// ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references @@ -30,8 +28,12 @@ import 'package:webview_pro_platform_interface/src/types/types.dart' as _i5; // 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 _FakeWidget_0 extends _i1.SmartFake implements _i2.Widget { + _FakeWidget_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); -class _FakeWidget_0 extends _i1.Fake implements _i2.Widget { @override String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => super.toString(); @@ -63,13 +65,21 @@ class MockWebViewPlatform extends _i1.Mock implements _i4.WebViewPlatform { #onWebViewPlatformCreated: onWebViewPlatformCreated, #gestureRecognizers: gestureRecognizers }), - returnValue: _FakeWidget_0()) as _i2.Widget); + returnValue: _FakeWidget_0( + this, + Invocation.method(#build, [], { + #context: context, + #creationParams: creationParams, + #webViewPlatformCallbacksHandler: + webViewPlatformCallbacksHandler, + #javascriptChannelRegistry: javascriptChannelRegistry, + #onWebViewPlatformCreated: onWebViewPlatformCreated, + #gestureRecognizers: gestureRecognizers + }))) as _i2.Widget); @override _i9.Future clearCookies() => (super.noSuchMethod(Invocation.method(#clearCookies, []), - returnValue: Future.value(false)) as _i9.Future); - @override - String toString() => super.toString(); + returnValue: _i9.Future.value(false)) as _i9.Future); } /// A class which mocks [WebViewPlatformController]. @@ -82,118 +92,122 @@ class MockWebViewPlatformController extends _i1.Mock } @override - _i9.Future loadFile(String? absoluteFilePath) => - (super.noSuchMethod(Invocation.method(#loadFile, [absoluteFilePath]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + _i9.Future loadFile(String? absoluteFilePath) => (super.noSuchMethod( + Invocation.method(#loadFile, [absoluteFilePath]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) as _i9.Future); @override - _i9.Future loadFlutterAsset(String? key) => - (super.noSuchMethod(Invocation.method(#loadFlutterAsset, [key]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + _i9.Future loadFlutterAsset(String? key) => (super.noSuchMethod( + Invocation.method(#loadFlutterAsset, [key]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) as _i9.Future); @override _i9.Future loadHtmlString(String? html, {String? baseUrl}) => (super.noSuchMethod( - Invocation.method(#loadHtmlString, [html], {#baseUrl: baseUrl}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + Invocation.method(#loadHtmlString, [html], {#baseUrl: baseUrl}), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) + as _i9.Future); @override _i9.Future loadUrl(String? url, Map? headers) => (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) + as _i9.Future); @override _i9.Future loadRequest(_i5.WebViewRequest? request) => (super.noSuchMethod(Invocation.method(#loadRequest, [request]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) + as _i9.Future); @override _i9.Future updateSettings(_i5.WebSettings? setting) => (super.noSuchMethod(Invocation.method(#updateSettings, [setting]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) + as _i9.Future); @override _i9.Future currentUrl() => (super.noSuchMethod(Invocation.method(#currentUrl, []), - returnValue: Future.value()) as _i9.Future); + returnValue: _i9.Future.value()) as _i9.Future); @override _i9.Future canGoBack() => (super.noSuchMethod(Invocation.method(#canGoBack, []), - returnValue: Future.value(false)) as _i9.Future); + returnValue: _i9.Future.value(false)) as _i9.Future); @override _i9.Future canGoForward() => (super.noSuchMethod(Invocation.method(#canGoForward, []), - returnValue: Future.value(false)) as _i9.Future); + returnValue: _i9.Future.value(false)) as _i9.Future); @override - _i9.Future goBack() => - (super.noSuchMethod(Invocation.method(#goBack, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + _i9.Future goBack() => (super.noSuchMethod( + Invocation.method(#goBack, []), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) as _i9.Future); @override - _i9.Future goForward() => - (super.noSuchMethod(Invocation.method(#goForward, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + _i9.Future goForward() => (super.noSuchMethod( + Invocation.method(#goForward, []), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) as _i9.Future); @override - _i9.Future reload() => - (super.noSuchMethod(Invocation.method(#reload, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + _i9.Future reload() => (super.noSuchMethod( + Invocation.method(#reload, []), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) as _i9.Future); @override - _i9.Future clearCache() => - (super.noSuchMethod(Invocation.method(#clearCache, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + _i9.Future clearCache() => (super.noSuchMethod( + Invocation.method(#clearCache, []), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) as _i9.Future); @override _i9.Future evaluateJavascript(String? javascript) => (super.noSuchMethod(Invocation.method(#evaluateJavascript, [javascript]), - returnValue: Future.value('')) as _i9.Future); + returnValue: _i9.Future.value('')) as _i9.Future); @override - _i9.Future runJavascript(String? javascript) => - (super.noSuchMethod(Invocation.method(#runJavascript, [javascript]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + _i9.Future runJavascript(String? javascript) => (super.noSuchMethod( + Invocation.method(#runJavascript, [javascript]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) as _i9.Future); @override _i9.Future runJavascriptReturningResult(String? javascript) => (super.noSuchMethod( Invocation.method(#runJavascriptReturningResult, [javascript]), - returnValue: Future.value('')) as _i9.Future); + returnValue: _i9.Future.value('')) as _i9.Future); @override _i9.Future addJavascriptChannels(Set? javascriptChannelNames) => (super.noSuchMethod( Invocation.method(#addJavascriptChannels, [javascriptChannelNames]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: + _i9.Future.value()) as _i9.Future); @override _i9.Future removeJavascriptChannels( Set? javascriptChannelNames) => (super.noSuchMethod( - Invocation.method( - #removeJavascriptChannels, [javascriptChannelNames]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + Invocation.method( + #removeJavascriptChannels, [javascriptChannelNames]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) + as _i9.Future); @override _i9.Future getTitle() => (super.noSuchMethod(Invocation.method(#getTitle, []), - returnValue: Future.value()) as _i9.Future); + returnValue: _i9.Future.value()) as _i9.Future); @override - _i9.Future scrollTo(int? x, int? y) => - (super.noSuchMethod(Invocation.method(#scrollTo, [x, y]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + _i9.Future scrollTo(int? x, int? y) => (super.noSuchMethod( + Invocation.method(#scrollTo, [x, y]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) as _i9.Future); @override - _i9.Future scrollBy(int? x, int? y) => - (super.noSuchMethod(Invocation.method(#scrollBy, [x, y]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); + _i9.Future scrollBy(int? x, int? y) => (super.noSuchMethod( + Invocation.method(#scrollBy, [x, y]), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value()) as _i9.Future); @override _i9.Future getScrollX() => (super.noSuchMethod(Invocation.method(#getScrollX, []), - returnValue: Future.value(0)) as _i9.Future); + returnValue: _i9.Future.value(0)) as _i9.Future); @override _i9.Future getScrollY() => (super.noSuchMethod(Invocation.method(#getScrollY, []), - returnValue: Future.value(0)) as _i9.Future); - @override - String toString() => super.toString(); + returnValue: _i9.Future.value(0)) as _i9.Future); } diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index e9b0203f4e90..aae2422163e3 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,113 @@ +## 2.10.4+1 + +* Merged changes from upsteam + +## 2.10.4 + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. +* Bumps androidx.annotation from 1.4.0 to 1.5.0. + +## 2.10.3 + +* Updates imports for `prefer_relative_imports`. + +## 2.10.2 + +* Adds a getter to expose the Java InstanceManager. + +## 2.10.1 + +* Adds a method to the `WebView` wrapper to retrieve the X and Y positions simultaneously. +* Removes reference to https://github.com/flutter/flutter/issues/97744 from `README`. + +## 2.10.0 + +* Bumps webkit from 1.0.0 to 1.5.0. +* Raises minimum `compileSdkVersion` to 32. + +## 2.9.5 + +* Adds dispose methods for HostApi and FlutterApi of JavaObject. + +## 2.9.4 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. +* Bumps gradle from 7.2.1 to 7.2.2. + +## 2.9.3 + +* Updates the Dart InstanceManager to take a listener for when an object is garbage collected. + See https://github.com/flutter/flutter/issues/107199. + +## 2.9.2 + +* Updates the Java InstanceManager to take a listener for when an object is garbage collected. + See https://github.com/flutter/flutter/issues/107199. + +## 2.9.1 + +* Updates Android WebView classes as Copyable. This is a part of moving the api to handle garbage + collection automatically. See https://github.com/flutter/flutter/issues/107199. + +## 2.9.0 + +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). +* Fixes bug where `Directionality` from context didn't affect `SurfaceAndroidWebView`. +* Fixes bug where default text direction was different for `SurfaceAndroidWebView` and `AndroidWebView`. + Default is now `TextDirection.ltr` for both. +* Fixes bug where setting WebView to a transparent background could cause visual errors when using + `SurfaceAndroidWebView`. Hybrid composition is now used when the background color is not 100% + opaque. +* Raises minimum Flutter version to 3.0.0. + +## 2.8.14 + +* Bumps androidx.annotation from 1.0.0 to 1.4.0. + +## 2.8.13 + +* Fixes a bug which causes an exception when the `onNavigationRequestCallback` return `false`. + +## 2.8.12 + +* Bumps mockito-inline from 3.11.1 to 4.6.1. + +## 2.8.11 + +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/104231). + +## 2.8.10 + +* Updates references to the obsolete master branch. + +## 2.8.9 + +* Updates Gradle to 7.2.1. + +## 2.8.8 + +* Minor fixes for new analysis options. + +## 2.8.7 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 2.8.6 + +* Updates pigeon developer dependency to the latest version which adds support for null safety. + +## 2.8.5 + +* Migrates deprecated `Scaffold.showSnackBar` to `ScaffoldMessenger` in example app. + +## 2.8.4 + +* Fixes bug preventing `mockito` code generation for tests. +* Fixes regression where local storage wasn't cleared when `WebViewController.clearCache` was + called. + ## 2.8.3+3 * Fixes file chooser AlertDialog.Builder displays blank on certain devices. diff --git a/packages/webview_flutter/webview_flutter_android/README.md b/packages/webview_flutter/webview_flutter_android/README.md index a92d4a226fc3..a99bf87331a6 100644 --- a/packages/webview_flutter/webview_flutter_android/README.md +++ b/packages/webview_flutter/webview_flutter_android/README.md @@ -9,11 +9,15 @@ normally. This package will be automatically included in your app when you do. ## Contributing -This package uses [pigeon][3] to generate the communication layer between Flutter and the host platform (Android). The communication interface is defined in the `pigeons/android_webview.dart` file. After editing the communication interface regenerate the communication layer by running the `./generatePigeons.sh` shell script. +This package uses [pigeon][3] to generate the communication layer between Flutter and the host +platform (Android). The communication interface is defined in the `pigeons/android_webview.dart` +file. After editing the communication interface regenerate the communication layer by running +`flutter pub run pigeon --input pigeons/android_webview.dart`. -Besides [pigeon][3] this package also uses [mockito][4] to generate mock objects for testing purposes. To generate the mock objects run the following command: +Besides [pigeon][3] this package also uses [mockito][4] to generate mock objects for testing +purposes. To generate the mock objects run the following command: ```bash -flutter packages pub run build_runner build --delete-conflicting-outputs +flutter pub run build_runner build --delete-conflicting-outputs ``` If you would like to contribute to the plugin, check out our [contribution guide][5]. @@ -22,5 +26,5 @@ If you would like to contribute to the plugin, check out our [contribution guide [2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin [3]: https://pub.dev/packages/pigeon [4]: https://pub.dev/packages/mockito -[5]: https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md +[5]: https://github.com/flutter/plugins/blob/main/CONTRIBUTING.md diff --git a/packages/webview_flutter/webview_flutter_android/android/build.gradle b/packages/webview_flutter/webview_flutter_android/android/build.gradle index 7676c4631a1a..7384f8d453da 100644 --- a/packages/webview_flutter/webview_flutter_android/android/build.gradle +++ b/packages/webview_flutter/webview_flutter_android/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'com.android.tools.build:gradle:7.2.2' } } @@ -22,7 +22,7 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 31 + compileSdkVersion 32 defaultConfig { minSdkVersion 19 @@ -30,15 +30,16 @@ android { } lintOptions { + disable 'AndroidGradlePluginVersion' disable 'InvalidPackage' disable 'GradleDependency' } dependencies { - implementation 'androidx.annotation:annotation:1.0.0' - implementation 'androidx.webkit:webkit:1.0.0' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:3.11.1' + 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 'androidx.test:core:1.3.0' } diff --git a/packages/webview_flutter/webview_flutter_android/android/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter_android/android/gradle/wrapper/gradle-wrapper.properties index ed1a787b3b22..b1159fc54f39 100644 --- a/packages/webview_flutter/webview_flutter_android/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/webview_flutter/webview_flutter_android/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java index 2dd98c47d582..1981d8eccf12 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java @@ -38,7 +38,7 @@ public void onDownloadStart( long contentLength, Reply callback) { onDownloadStart( - instanceManager.getInstanceId(downloadListener), + getIdentifierForListener(downloadListener), url, userAgent, contentDisposition, @@ -54,11 +54,18 @@ public void onDownloadStart( * @param callback reply callback with return value from Dart */ public void dispose(DownloadListener downloadListener, Reply callback) { - final Long instanceId = instanceManager.removeInstance(downloadListener); - if (instanceId != null) { - dispose(instanceId, callback); + if (instanceManager.containsInstance(downloadListener)) { + dispose(getIdentifierForListener(downloadListener), callback); } else { callback.reply(null); } } + + private long getIdentifierForListener(DownloadListener listener) { + final Long identifier = instanceManager.getIdentifierForStrongReference(listener); + if (identifier == null) { + throw new IllegalStateException("Could not find identifier for DownloadListener."); + } + return identifier; + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java index 9694f396ad2e..ed0c2aee4700 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java @@ -91,6 +91,6 @@ public DownloadListenerHostApiImpl( public void create(Long instanceId) { final DownloadListener downloadListener = downloadListenerCreator.createDownloadListener(flutterApi); - instanceManager.addInstance(downloadListener, instanceId); + instanceManager.addDartCreatedInstance(downloadListener, 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 ee55e1e02af0..cd3db00288c8 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,8 +1,14 @@ -// Autogenerated from Pigeon (v1.0.9), do not edit directly. +// 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. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.webviewflutter; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.plugin.common.BasicMessageChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MessageCodec; @@ -11,41 +17,113 @@ import java.nio.ByteBuffer; import java.util.Arrays; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.HashMap; -/** Generated class from Pigeon. */ +/**Generated class from Pigeon. */ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) public class GeneratedAndroidWebView { /** Generated class from Pigeon that represents data sent in messages. */ public static class WebResourceRequestData { - private String url; - public String getUrl() { return url; } - public void setUrl(String setterArg) { this.url = setterArg; } + private @NonNull String url; + public @NonNull String getUrl() { return url; } + public void setUrl(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"url\" is null."); + } + this.url = setterArg; + } - private Boolean isForMainFrame; - public Boolean getIsForMainFrame() { return isForMainFrame; } - public void setIsForMainFrame(Boolean setterArg) { this.isForMainFrame = setterArg; } + private @NonNull Boolean isForMainFrame; + public @NonNull Boolean getIsForMainFrame() { return isForMainFrame; } + public void setIsForMainFrame(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"isForMainFrame\" is null."); + } + this.isForMainFrame = setterArg; + } - private Boolean isRedirect; - public Boolean getIsRedirect() { return isRedirect; } - public void setIsRedirect(Boolean setterArg) { this.isRedirect = setterArg; } + private @Nullable Boolean isRedirect; + public @Nullable Boolean getIsRedirect() { return isRedirect; } + public void setIsRedirect(@Nullable Boolean setterArg) { + this.isRedirect = setterArg; + } - private Boolean hasGesture; - public Boolean getHasGesture() { return hasGesture; } - public void setHasGesture(Boolean setterArg) { this.hasGesture = setterArg; } + private @NonNull Boolean hasGesture; + public @NonNull Boolean getHasGesture() { return hasGesture; } + public void setHasGesture(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"hasGesture\" is null."); + } + this.hasGesture = setterArg; + } - private String method; - public String getMethod() { return method; } - public void setMethod(String setterArg) { this.method = setterArg; } + private @NonNull String method; + public @NonNull String getMethod() { return method; } + public void setMethod(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"method\" is null."); + } + this.method = setterArg; + } - private Map requestHeaders; - public Map getRequestHeaders() { return requestHeaders; } - public void setRequestHeaders(Map setterArg) { this.requestHeaders = setterArg; } + private @NonNull Map requestHeaders; + public @NonNull Map getRequestHeaders() { return requestHeaders; } + public void setRequestHeaders(@NonNull Map setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"requestHeaders\" is null."); + } + this.requestHeaders = setterArg; + } - Map toMap() { + /**Constructor is private to enforce null safety; use Builder. */ + private WebResourceRequestData() {} + public static final class Builder { + private @Nullable String url; + public @NonNull Builder setUrl(@NonNull String setterArg) { + this.url = setterArg; + return this; + } + private @Nullable Boolean isForMainFrame; + public @NonNull Builder setIsForMainFrame(@NonNull Boolean setterArg) { + this.isForMainFrame = setterArg; + return this; + } + private @Nullable Boolean isRedirect; + public @NonNull Builder setIsRedirect(@Nullable Boolean setterArg) { + this.isRedirect = setterArg; + return this; + } + private @Nullable Boolean hasGesture; + public @NonNull Builder setHasGesture(@NonNull Boolean setterArg) { + this.hasGesture = setterArg; + return this; + } + private @Nullable String method; + public @NonNull Builder setMethod(@NonNull String setterArg) { + this.method = setterArg; + return this; + } + private @Nullable Map requestHeaders; + public @NonNull Builder setRequestHeaders(@NonNull Map setterArg) { + this.requestHeaders = setterArg; + return this; + } + public @NonNull WebResourceRequestData build() { + WebResourceRequestData pigeonReturn = new WebResourceRequestData(); + pigeonReturn.setUrl(url); + pigeonReturn.setIsForMainFrame(isForMainFrame); + pigeonReturn.setIsRedirect(isRedirect); + pigeonReturn.setHasGesture(hasGesture); + pigeonReturn.setMethod(method); + pigeonReturn.setRequestHeaders(requestHeaders); + return pigeonReturn; + } + } + @NonNull Map toMap() { Map toMapResult = new HashMap<>(); toMapResult.put("url", url); toMapResult.put("isForMainFrame", isForMainFrame); @@ -55,47 +133,133 @@ Map toMap() { toMapResult.put("requestHeaders", requestHeaders); return toMapResult; } - static WebResourceRequestData fromMap(Map map) { - WebResourceRequestData fromMapResult = new WebResourceRequestData(); + static @NonNull WebResourceRequestData fromMap(@NonNull Map map) { + WebResourceRequestData pigeonResult = new WebResourceRequestData(); Object url = map.get("url"); - fromMapResult.url = (String)url; + pigeonResult.setUrl((String)url); Object isForMainFrame = map.get("isForMainFrame"); - fromMapResult.isForMainFrame = (Boolean)isForMainFrame; + pigeonResult.setIsForMainFrame((Boolean)isForMainFrame); Object isRedirect = map.get("isRedirect"); - fromMapResult.isRedirect = (Boolean)isRedirect; + pigeonResult.setIsRedirect((Boolean)isRedirect); Object hasGesture = map.get("hasGesture"); - fromMapResult.hasGesture = (Boolean)hasGesture; + pigeonResult.setHasGesture((Boolean)hasGesture); Object method = map.get("method"); - fromMapResult.method = (String)method; + pigeonResult.setMethod((String)method); Object requestHeaders = map.get("requestHeaders"); - fromMapResult.requestHeaders = (Map)requestHeaders; - return fromMapResult; + pigeonResult.setRequestHeaders((Map)requestHeaders); + return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ public static class WebResourceErrorData { - private Long errorCode; - public Long getErrorCode() { return errorCode; } - public void setErrorCode(Long setterArg) { this.errorCode = setterArg; } + private @NonNull Long errorCode; + public @NonNull Long getErrorCode() { return errorCode; } + public void setErrorCode(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"errorCode\" is null."); + } + this.errorCode = setterArg; + } - private String description; - public String getDescription() { return description; } - public void setDescription(String setterArg) { this.description = 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; + } - Map toMap() { + /**Constructor is private to enforce null safety; use Builder. */ + private WebResourceErrorData() {} + public static final class Builder { + private @Nullable Long errorCode; + public @NonNull Builder setErrorCode(@NonNull Long setterArg) { + this.errorCode = setterArg; + return this; + } + private @Nullable String description; + public @NonNull Builder setDescription(@NonNull String setterArg) { + this.description = setterArg; + return this; + } + public @NonNull WebResourceErrorData build() { + WebResourceErrorData pigeonReturn = new WebResourceErrorData(); + 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 WebResourceErrorData fromMap(Map map) { - WebResourceErrorData fromMapResult = new WebResourceErrorData(); + static @NonNull WebResourceErrorData fromMap(@NonNull Map map) { + WebResourceErrorData pigeonResult = new WebResourceErrorData(); Object errorCode = map.get("errorCode"); - fromMapResult.errorCode = (errorCode == null) ? null : ((errorCode instanceof Integer) ? (Integer)errorCode : (Long)errorCode); + pigeonResult.setErrorCode((errorCode == null) ? null : ((errorCode instanceof Integer) ? (Integer)errorCode : (Long)errorCode)); Object description = map.get("description"); - fromMapResult.description = (String)description; - return fromMapResult; + pigeonResult.setDescription((String)description); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class WebViewPoint { + private @NonNull Long x; + public @NonNull Long getX() { return x; } + public void setX(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"x\" is null."); + } + this.x = setterArg; + } + + private @NonNull Long y; + public @NonNull Long getY() { return y; } + public void setY(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"y\" is null."); + } + this.y = setterArg; + } + + /**Constructor is private to enforce null safety; use Builder. */ + private WebViewPoint() {} + public static final class Builder { + private @Nullable Long x; + public @NonNull Builder setX(@NonNull Long setterArg) { + this.x = setterArg; + return this; + } + private @Nullable Long y; + public @NonNull Builder setY(@NonNull Long setterArg) { + this.y = setterArg; + return this; + } + public @NonNull WebViewPoint build() { + WebViewPoint pigeonReturn = new WebViewPoint(); + pigeonReturn.setX(x); + pigeonReturn.setY(y); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("x", x); + toMapResult.put("y", y); + return toMapResult; + } + static @NonNull WebViewPoint fromMap(@NonNull Map map) { + WebViewPoint pigeonResult = new WebViewPoint(); + Object x = map.get("x"); + pigeonResult.setX((x == null) ? null : ((x instanceof Integer) ? (Integer)x : (Long)x)); + Object y = map.get("y"); + pigeonResult.setY((y == null) ? null : ((y instanceof Integer) ? (Integer)y : (Long)y)); + return pigeonResult; } } @@ -103,22 +267,86 @@ public interface Result { void success(T result); void error(Throwable error); } - private static class CookieManagerHostApiCodec extends StandardMessageCodec { - public static final CookieManagerHostApiCodec INSTANCE = new CookieManagerHostApiCodec(); - private CookieManagerHostApiCodec() {} - } + /** + * Handles methods calls to the native Java Object class. + * + * Also handles calls to remove the reference to an instance with `dispose`. + * + * See https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html. + * + * Generated interface from Pigeon that represents a handler of messages from Flutter. + */ + public interface JavaObjectHostApi { + void dispose(@NonNull Long identifier); - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** The codec used by JavaObjectHostApi. */ + static MessageCodec getCodec() { + return new StandardMessageCodec(); } + /**Sets up an instance of `JavaObjectHostApi` to handle messages through the `binaryMessenger`. */ + static void setup(BinaryMessenger binaryMessenger, JavaObjectHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.JavaObjectHostApi.dispose", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + assert args != null; + Number identifierArg = (Number)args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + api.dispose((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); + } + } + } + } + /** + * Handles callbacks methods for the native Java Object class. + * + * See https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html. + * + * Generated class from Pigeon that represents Flutter messages that can be called from Java. + */ + public static class JavaObjectFlutterApi { + private final BinaryMessenger binaryMessenger; + public JavaObjectFlutterApi(BinaryMessenger argBinaryMessenger){ + this.binaryMessenger = argBinaryMessenger; + } + public interface Reply { + void reply(T reply); + } + /** The codec used by JavaObjectFlutterApi. */ + static MessageCodec getCodec() { + return new StandardMessageCodec(); + } + public void dispose(@NonNull Long identifierArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.JavaObjectFlutterApi.dispose", getCodec()); + channel.send(new ArrayList(Collections.singletonList(identifierArg)), channelReply -> { + callback.reply(null); + }); + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface CookieManagerHostApi { void clearCookies(Result result); - void setCookie(String url, String value); + void setCookie(@NonNull String url, @NonNull String value); /** The codec used by CookieManagerHostApi. */ static MessageCodec getCodec() { - return CookieManagerHostApiCodec.INSTANCE; - } - - /** Sets up an instance of `CookieManagerHostApi` to handle messages through the `binaryMessenger`. */ + return new StandardMessageCodec(); } + /**Sets up an instance of `CookieManagerHostApi` to handle messages through the `binaryMessenger`. */ static void setup(BinaryMessenger binaryMessenger, CookieManagerHostApi api) { { BasicMessageChannel channel = @@ -157,6 +385,7 @@ public void error(Throwable error) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; String urlArg = (String)args.get(0); if (urlArg == null) { throw new NullPointerException("urlArg unexpectedly null."); @@ -182,43 +411,63 @@ public void error(Throwable error) { private static class WebViewHostApiCodec extends StandardMessageCodec { public static final WebViewHostApiCodec INSTANCE = new WebViewHostApiCodec(); private WebViewHostApiCodec() {} + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte)128: + return WebViewPoint.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + + } + } + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof WebViewPoint) { + stream.write(128); + writeValue(stream, ((WebViewPoint) value).toMap()); + } else +{ + super.writeValue(stream, value); + } + } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebViewHostApi { - void create(Long instanceId, Boolean useHybridComposition); - void dispose(Long instanceId); - void loadData(Long instanceId, String data, String mimeType, String encoding); - void loadDataWithBaseUrl(Long instanceId, String baseUrl, String data, String mimeType, String encoding, String historyUrl); - void loadUrl(Long instanceId, String url, Map headers); - void postUrl(Long instanceId, String url, byte[] data); - String getUrl(Long instanceId); - Boolean canGoBack(Long instanceId); - Boolean canGoForward(Long instanceId); - void goBack(Long instanceId); - void goForward(Long instanceId); - void reload(Long instanceId); - void clearCache(Long instanceId, Boolean includeDiskFiles); - void evaluateJavascript(Long instanceId, String javascriptString, Result result); - String getTitle(Long instanceId); - void scrollTo(Long instanceId, Long x, Long y); - void scrollBy(Long instanceId, Long x, Long y); - Long getScrollX(Long instanceId); - Long getScrollY(Long instanceId); - void setWebContentsDebuggingEnabled(Boolean enabled); - void setWebViewClient(Long instanceId, Long webViewClientInstanceId); - void addJavaScriptChannel(Long instanceId, Long javaScriptChannelInstanceId); - void removeJavaScriptChannel(Long instanceId, Long javaScriptChannelInstanceId); - void setDownloadListener(Long instanceId, Long listenerInstanceId); - void setWebChromeClient(Long instanceId, Long clientInstanceId); - void setBackgroundColor(Long instanceId, Long color); + void create(@NonNull Long instanceId, @NonNull Boolean useHybridComposition); + void dispose(@NonNull Long instanceId); + void loadData(@NonNull Long instanceId, @NonNull String data, @Nullable String mimeType, @Nullable String encoding); + void loadDataWithBaseUrl(@NonNull Long instanceId, @Nullable String baseUrl, @NonNull String data, @Nullable String mimeType, @Nullable String encoding, @Nullable String historyUrl); + void loadUrl(@NonNull Long instanceId, @NonNull String url, @NonNull Map headers); + void postUrl(@NonNull Long instanceId, @NonNull String url, @NonNull byte[] data); + @Nullable String getUrl(@NonNull Long instanceId); + @NonNull Boolean canGoBack(@NonNull Long instanceId); + @NonNull Boolean canGoForward(@NonNull Long instanceId); + void goBack(@NonNull Long instanceId); + void goForward(@NonNull Long instanceId); + void reload(@NonNull Long instanceId); + void clearCache(@NonNull Long instanceId, @NonNull Boolean includeDiskFiles); + void evaluateJavascript(@NonNull Long instanceId, @NonNull String javascriptString, Result result); + @Nullable String getTitle(@NonNull Long instanceId); + void scrollTo(@NonNull Long instanceId, @NonNull Long x, @NonNull Long y); + void scrollBy(@NonNull Long instanceId, @NonNull Long x, @NonNull Long y); + @NonNull Long getScrollX(@NonNull Long instanceId); + @NonNull Long getScrollY(@NonNull Long instanceId); + @NonNull WebViewPoint getScrollPosition(@NonNull Long instanceId); + void setWebContentsDebuggingEnabled(@NonNull Boolean enabled); + void setWebViewClient(@NonNull Long instanceId, @NonNull Long webViewClientInstanceId); + void addJavaScriptChannel(@NonNull Long instanceId, @NonNull Long javaScriptChannelInstanceId); + void removeJavaScriptChannel(@NonNull Long instanceId, @NonNull Long javaScriptChannelInstanceId); + void setDownloadListener(@NonNull Long instanceId, @Nullable Long listenerInstanceId); + void setWebChromeClient(@NonNull Long instanceId, @Nullable Long clientInstanceId); + void setBackgroundColor(@NonNull Long instanceId, @NonNull Long color); /** The codec used by WebViewHostApi. */ static MessageCodec getCodec() { - return WebViewHostApiCodec.INSTANCE; - } - - /** Sets up an instance of `WebViewHostApi` to handle messages through the `binaryMessenger`. */ + return WebViewHostApiCodec.INSTANCE; } + /**Sets up an instance of `WebViewHostApi` to handle messages through the `binaryMessenger`. */ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { { BasicMessageChannel channel = @@ -228,6 +477,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -236,7 +486,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (useHybridCompositionArg == null) { throw new NullPointerException("useHybridCompositionArg unexpectedly null."); } - api.create(instanceIdArg.longValue(), useHybridCompositionArg); + api.create((instanceIdArg == null) ? null : instanceIdArg.longValue(), useHybridCompositionArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -256,11 +506,12 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.dispose(instanceIdArg.longValue()); + api.dispose((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -280,6 +531,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -289,14 +541,8 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { throw new NullPointerException("dataArg unexpectedly null."); } String mimeTypeArg = (String)args.get(2); - if (mimeTypeArg == null) { - throw new NullPointerException("mimeTypeArg unexpectedly null."); - } String encodingArg = (String)args.get(3); - if (encodingArg == null) { - throw new NullPointerException("encodingArg unexpectedly null."); - } - api.loadData(instanceIdArg.longValue(), dataArg, mimeTypeArg, encodingArg); + api.loadData((instanceIdArg == null) ? null : instanceIdArg.longValue(), dataArg, mimeTypeArg, encodingArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -316,31 +562,20 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } String baseUrlArg = (String)args.get(1); - if (baseUrlArg == null) { - throw new NullPointerException("baseUrlArg unexpectedly null."); - } String dataArg = (String)args.get(2); if (dataArg == null) { throw new NullPointerException("dataArg unexpectedly null."); } String mimeTypeArg = (String)args.get(3); - if (mimeTypeArg == null) { - throw new NullPointerException("mimeTypeArg unexpectedly null."); - } String encodingArg = (String)args.get(4); - if (encodingArg == null) { - throw new NullPointerException("encodingArg unexpectedly null."); - } String historyUrlArg = (String)args.get(5); - if (historyUrlArg == null) { - throw new NullPointerException("historyUrlArg unexpectedly null."); - } - api.loadDataWithBaseUrl(instanceIdArg.longValue(), baseUrlArg, dataArg, mimeTypeArg, encodingArg, historyUrlArg); + api.loadDataWithBaseUrl((instanceIdArg == null) ? null : instanceIdArg.longValue(), baseUrlArg, dataArg, mimeTypeArg, encodingArg, historyUrlArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -360,6 +595,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -372,7 +608,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (headersArg == null) { throw new NullPointerException("headersArg unexpectedly null."); } - api.loadUrl(instanceIdArg.longValue(), urlArg, headersArg); + api.loadUrl((instanceIdArg == null) ? null : instanceIdArg.longValue(), urlArg, headersArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -392,6 +628,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -404,7 +641,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (dataArg == null) { throw new NullPointerException("dataArg unexpectedly null."); } - api.postUrl(instanceIdArg.longValue(), urlArg, dataArg); + api.postUrl((instanceIdArg == null) ? null : instanceIdArg.longValue(), urlArg, dataArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -424,11 +661,12 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - String output = api.getUrl(instanceIdArg.longValue()); + String output = api.getUrl((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", output); } catch (Error | RuntimeException exception) { @@ -448,11 +686,12 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - Boolean output = api.canGoBack(instanceIdArg.longValue()); + Boolean output = api.canGoBack((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", output); } catch (Error | RuntimeException exception) { @@ -472,11 +711,12 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - Boolean output = api.canGoForward(instanceIdArg.longValue()); + Boolean output = api.canGoForward((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", output); } catch (Error | RuntimeException exception) { @@ -496,11 +736,12 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.goBack(instanceIdArg.longValue()); + api.goBack((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -520,11 +761,12 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.goForward(instanceIdArg.longValue()); + api.goForward((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -544,11 +786,12 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.reload(instanceIdArg.longValue()); + api.reload((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -568,6 +811,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -576,7 +820,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (includeDiskFilesArg == null) { throw new NullPointerException("includeDiskFilesArg unexpectedly null."); } - api.clearCache(instanceIdArg.longValue(), includeDiskFilesArg); + api.clearCache((instanceIdArg == null) ? null : instanceIdArg.longValue(), includeDiskFilesArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -596,6 +840,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -615,7 +860,7 @@ public void error(Throwable error) { } }; - api.evaluateJavascript(instanceIdArg.longValue(), javascriptStringArg, resultCallback); + api.evaluateJavascript((instanceIdArg == null) ? null : instanceIdArg.longValue(), javascriptStringArg, resultCallback); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); @@ -634,11 +879,12 @@ public void error(Throwable error) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - String output = api.getTitle(instanceIdArg.longValue()); + String output = api.getTitle((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", output); } catch (Error | RuntimeException exception) { @@ -658,6 +904,7 @@ public void error(Throwable error) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -670,7 +917,7 @@ public void error(Throwable error) { if (yArg == null) { throw new NullPointerException("yArg unexpectedly null."); } - api.scrollTo(instanceIdArg.longValue(), xArg.longValue(), yArg.longValue()); + api.scrollTo((instanceIdArg == null) ? null : instanceIdArg.longValue(), (xArg == null) ? null : xArg.longValue(), (yArg == null) ? null : yArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -690,6 +937,7 @@ public void error(Throwable error) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -702,7 +950,7 @@ public void error(Throwable error) { if (yArg == null) { throw new NullPointerException("yArg unexpectedly null."); } - api.scrollBy(instanceIdArg.longValue(), xArg.longValue(), yArg.longValue()); + api.scrollBy((instanceIdArg == null) ? null : instanceIdArg.longValue(), (xArg == null) ? null : xArg.longValue(), (yArg == null) ? null : yArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -722,11 +970,12 @@ public void error(Throwable error) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - Long output = api.getScrollX(instanceIdArg.longValue()); + Long output = api.getScrollX((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", output); } catch (Error | RuntimeException exception) { @@ -746,11 +995,37 @@ public void error(Throwable error) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - Long output = api.getScrollY(instanceIdArg.longValue()); + Long output = api.getScrollY((instanceIdArg == null) ? null : instanceIdArg.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.WebViewHostApi.getScrollPosition", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + assert args != null; + Number instanceIdArg = (Number)args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + WebViewPoint output = api.getScrollPosition((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", output); } catch (Error | RuntimeException exception) { @@ -770,6 +1045,7 @@ public void error(Throwable error) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Boolean enabledArg = (Boolean)args.get(0); if (enabledArg == null) { throw new NullPointerException("enabledArg unexpectedly null."); @@ -794,6 +1070,7 @@ public void error(Throwable error) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -802,7 +1079,7 @@ public void error(Throwable error) { if (webViewClientInstanceIdArg == null) { throw new NullPointerException("webViewClientInstanceIdArg unexpectedly null."); } - api.setWebViewClient(instanceIdArg.longValue(), webViewClientInstanceIdArg.longValue()); + api.setWebViewClient((instanceIdArg == null) ? null : instanceIdArg.longValue(), (webViewClientInstanceIdArg == null) ? null : webViewClientInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -822,6 +1099,7 @@ public void error(Throwable error) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -830,7 +1108,7 @@ public void error(Throwable error) { if (javaScriptChannelInstanceIdArg == null) { throw new NullPointerException("javaScriptChannelInstanceIdArg unexpectedly null."); } - api.addJavaScriptChannel(instanceIdArg.longValue(), javaScriptChannelInstanceIdArg.longValue()); + api.addJavaScriptChannel((instanceIdArg == null) ? null : instanceIdArg.longValue(), (javaScriptChannelInstanceIdArg == null) ? null : javaScriptChannelInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -850,6 +1128,7 @@ public void error(Throwable error) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -858,7 +1137,7 @@ public void error(Throwable error) { if (javaScriptChannelInstanceIdArg == null) { throw new NullPointerException("javaScriptChannelInstanceIdArg unexpectedly null."); } - api.removeJavaScriptChannel(instanceIdArg.longValue(), javaScriptChannelInstanceIdArg.longValue()); + api.removeJavaScriptChannel((instanceIdArg == null) ? null : instanceIdArg.longValue(), (javaScriptChannelInstanceIdArg == null) ? null : javaScriptChannelInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -878,15 +1157,13 @@ public void error(Throwable error) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } Number listenerInstanceIdArg = (Number)args.get(1); - if (listenerInstanceIdArg == null) { - throw new NullPointerException("listenerInstanceIdArg unexpectedly null."); - } - api.setDownloadListener(instanceIdArg.longValue(), listenerInstanceIdArg.longValue()); + api.setDownloadListener((instanceIdArg == null) ? null : instanceIdArg.longValue(), (listenerInstanceIdArg == null) ? null : listenerInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -906,15 +1183,13 @@ public void error(Throwable error) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } Number clientInstanceIdArg = (Number)args.get(1); - if (clientInstanceIdArg == null) { - throw new NullPointerException("clientInstanceIdArg unexpectedly null."); - } - api.setWebChromeClient(instanceIdArg.longValue(), clientInstanceIdArg.longValue()); + api.setWebChromeClient((instanceIdArg == null) ? null : instanceIdArg.longValue(), (clientInstanceIdArg == null) ? null : clientInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -934,6 +1209,7 @@ public void error(Throwable error) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -942,7 +1218,7 @@ public void error(Throwable error) { if (colorArg == null) { throw new NullPointerException("colorArg unexpectedly null."); } - api.setBackgroundColor(instanceIdArg.longValue(), colorArg.longValue()); + api.setBackgroundColor((instanceIdArg == null) ? null : instanceIdArg.longValue(), (colorArg == null) ? null : colorArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -956,35 +1232,28 @@ public void error(Throwable error) { } } } - private static class WebSettingsHostApiCodec extends StandardMessageCodec { - public static final WebSettingsHostApiCodec INSTANCE = new WebSettingsHostApiCodec(); - private WebSettingsHostApiCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebSettingsHostApi { - void create(Long instanceId, Long webViewInstanceId); - void dispose(Long instanceId); - void setDomStorageEnabled(Long instanceId, Boolean flag); - void setJavaScriptCanOpenWindowsAutomatically(Long instanceId, Boolean flag); - void setSupportMultipleWindows(Long instanceId, Boolean support); - void setJavaScriptEnabled(Long instanceId, Boolean flag); - void setUserAgentString(Long instanceId, String userAgentString); - void setMediaPlaybackRequiresUserGesture(Long instanceId, Boolean require); - void setSupportZoom(Long instanceId, Boolean support); - void setLoadWithOverviewMode(Long instanceId, Boolean overview); - void setUseWideViewPort(Long instanceId, Boolean use); - void setDisplayZoomControls(Long instanceId, Boolean enabled); - void setBuiltInZoomControls(Long instanceId, Boolean enabled); - void setAllowFileAccess(Long instanceId, Boolean enabled); - void setGeolocationEnabled(Long instanceId, Boolean enabled); + void create(@NonNull Long instanceId, @NonNull Long webViewInstanceId); + void dispose(@NonNull Long instanceId); + void setDomStorageEnabled(@NonNull Long instanceId, @NonNull Boolean flag); + void setJavaScriptCanOpenWindowsAutomatically(@NonNull Long instanceId, @NonNull Boolean flag); + void setSupportMultipleWindows(@NonNull Long instanceId, @NonNull Boolean support); + void setJavaScriptEnabled(@NonNull Long instanceId, @NonNull Boolean flag); + void setUserAgentString(@NonNull Long instanceId, @Nullable String userAgentString); + void setMediaPlaybackRequiresUserGesture(@NonNull Long instanceId, @NonNull Boolean require); + void setSupportZoom(@NonNull Long instanceId, @NonNull Boolean support); + void setLoadWithOverviewMode(@NonNull Long instanceId, @NonNull Boolean overview); + void setUseWideViewPort(@NonNull Long instanceId, @NonNull Boolean use); + void setDisplayZoomControls(@NonNull Long instanceId, @NonNull Boolean enabled); + void setBuiltInZoomControls(@NonNull Long instanceId, @NonNull Boolean enabled); + void setAllowFileAccess(@NonNull Long instanceId, @NonNull Boolean enabled); + void setGeolocationEnabled(@NonNull Long instanceId, @NonNull Boolean enabled); /** The codec used by WebSettingsHostApi. */ static MessageCodec getCodec() { - return WebSettingsHostApiCodec.INSTANCE; - } - - /** Sets up an instance of `WebSettingsHostApi` to handle messages through the `binaryMessenger`. */ + return new StandardMessageCodec(); } + /**Sets up an instance of `WebSettingsHostApi` to handle messages through the `binaryMessenger`. */ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { { BasicMessageChannel channel = @@ -994,6 +1263,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1002,7 +1272,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (webViewInstanceIdArg == null) { throw new NullPointerException("webViewInstanceIdArg unexpectedly null."); } - api.create(instanceIdArg.longValue(), webViewInstanceIdArg.longValue()); + api.create((instanceIdArg == null) ? null : instanceIdArg.longValue(), (webViewInstanceIdArg == null) ? null : webViewInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1022,11 +1292,12 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.dispose(instanceIdArg.longValue()); + api.dispose((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1046,6 +1317,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1054,7 +1326,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (flagArg == null) { throw new NullPointerException("flagArg unexpectedly null."); } - api.setDomStorageEnabled(instanceIdArg.longValue(), flagArg); + api.setDomStorageEnabled((instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1074,6 +1346,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1082,7 +1355,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (flagArg == null) { throw new NullPointerException("flagArg unexpectedly null."); } - api.setJavaScriptCanOpenWindowsAutomatically(instanceIdArg.longValue(), flagArg); + api.setJavaScriptCanOpenWindowsAutomatically((instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1102,6 +1375,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1110,7 +1384,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (supportArg == null) { throw new NullPointerException("supportArg unexpectedly null."); } - api.setSupportMultipleWindows(instanceIdArg.longValue(), supportArg); + api.setSupportMultipleWindows((instanceIdArg == null) ? null : instanceIdArg.longValue(), supportArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1130,6 +1404,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1138,7 +1413,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (flagArg == null) { throw new NullPointerException("flagArg unexpectedly null."); } - api.setJavaScriptEnabled(instanceIdArg.longValue(), flagArg); + api.setJavaScriptEnabled((instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1158,15 +1433,13 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } String userAgentStringArg = (String)args.get(1); - if (userAgentStringArg == null) { - throw new NullPointerException("userAgentStringArg unexpectedly null."); - } - api.setUserAgentString(instanceIdArg.longValue(), userAgentStringArg); + api.setUserAgentString((instanceIdArg == null) ? null : instanceIdArg.longValue(), userAgentStringArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1186,6 +1459,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1194,7 +1468,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (requireArg == null) { throw new NullPointerException("requireArg unexpectedly null."); } - api.setMediaPlaybackRequiresUserGesture(instanceIdArg.longValue(), requireArg); + api.setMediaPlaybackRequiresUserGesture((instanceIdArg == null) ? null : instanceIdArg.longValue(), requireArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1214,6 +1488,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1222,7 +1497,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (supportArg == null) { throw new NullPointerException("supportArg unexpectedly null."); } - api.setSupportZoom(instanceIdArg.longValue(), supportArg); + api.setSupportZoom((instanceIdArg == null) ? null : instanceIdArg.longValue(), supportArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1242,6 +1517,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1250,7 +1526,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (overviewArg == null) { throw new NullPointerException("overviewArg unexpectedly null."); } - api.setLoadWithOverviewMode(instanceIdArg.longValue(), overviewArg); + api.setLoadWithOverviewMode((instanceIdArg == null) ? null : instanceIdArg.longValue(), overviewArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1270,6 +1546,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1278,7 +1555,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (useArg == null) { throw new NullPointerException("useArg unexpectedly null."); } - api.setUseWideViewPort(instanceIdArg.longValue(), useArg); + api.setUseWideViewPort((instanceIdArg == null) ? null : instanceIdArg.longValue(), useArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1298,6 +1575,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1306,7 +1584,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (enabledArg == null) { throw new NullPointerException("enabledArg unexpectedly null."); } - api.setDisplayZoomControls(instanceIdArg.longValue(), enabledArg); + api.setDisplayZoomControls((instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1326,6 +1604,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1334,7 +1613,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (enabledArg == null) { throw new NullPointerException("enabledArg unexpectedly null."); } - api.setBuiltInZoomControls(instanceIdArg.longValue(), enabledArg); + api.setBuiltInZoomControls((instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1354,6 +1633,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1362,7 +1642,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (enabledArg == null) { throw new NullPointerException("enabledArg unexpectedly null."); } - api.setAllowFileAccess(instanceIdArg.longValue(), enabledArg); + api.setAllowFileAccess((instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1382,6 +1662,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1390,7 +1671,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (enabledArg == null) { throw new NullPointerException("enabledArg unexpectedly null."); } - api.setGeolocationEnabled(instanceIdArg.longValue(), enabledArg); + api.setGeolocationEnabled((instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1404,21 +1685,14 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } } } - private static class JavaScriptChannelHostApiCodec extends StandardMessageCodec { - public static final JavaScriptChannelHostApiCodec INSTANCE = new JavaScriptChannelHostApiCodec(); - private JavaScriptChannelHostApiCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface JavaScriptChannelHostApi { - void create(Long instanceId, String channelName); + void create(@NonNull Long instanceId, @NonNull String channelName); /** The codec used by JavaScriptChannelHostApi. */ static MessageCodec getCodec() { - return JavaScriptChannelHostApiCodec.INSTANCE; - } - - /** Sets up an instance of `JavaScriptChannelHostApi` to handle messages through the `binaryMessenger`. */ + return new StandardMessageCodec(); } + /**Sets up an instance of `JavaScriptChannelHostApi` to handle messages through the `binaryMessenger`. */ static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) { { BasicMessageChannel channel = @@ -1428,6 +1702,7 @@ static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1436,7 +1711,7 @@ static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) if (channelNameArg == null) { throw new NullPointerException("channelNameArg unexpectedly null."); } - api.create(instanceIdArg.longValue(), channelNameArg); + api.create((instanceIdArg == null) ? null : instanceIdArg.longValue(), channelNameArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1450,12 +1725,7 @@ static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) } } } - private static class JavaScriptChannelFlutterApiCodec extends StandardMessageCodec { - public static final JavaScriptChannelFlutterApiCodec INSTANCE = new JavaScriptChannelFlutterApiCodec(); - private JavaScriptChannelFlutterApiCodec() {} - } - - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class JavaScriptChannelFlutterApi { private final BinaryMessenger binaryMessenger; public JavaScriptChannelFlutterApi(BinaryMessenger argBinaryMessenger){ @@ -1464,18 +1734,18 @@ public JavaScriptChannelFlutterApi(BinaryMessenger argBinaryMessenger){ public interface Reply { void reply(T reply); } + /** The codec used by JavaScriptChannelFlutterApi. */ static MessageCodec getCodec() { - return JavaScriptChannelFlutterApiCodec.INSTANCE; + return new StandardMessageCodec(); } - - public void dispose(Long instanceIdArg, Reply callback) { + public void dispose(@NonNull Long instanceIdArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose", getCodec()); - channel.send(new ArrayList(Arrays.asList(instanceIdArg)), channelReply -> { + channel.send(new ArrayList(Collections.singletonList(instanceIdArg)), channelReply -> { callback.reply(null); }); } - public void postMessage(Long instanceIdArg, String messageArg, Reply callback) { + public void postMessage(@NonNull Long instanceIdArg, @NonNull String messageArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage", getCodec()); channel.send(new ArrayList(Arrays.asList(instanceIdArg, messageArg)), channelReply -> { @@ -1483,21 +1753,14 @@ public void postMessage(Long instanceIdArg, String messageArg, Reply callb }); } } - private static class WebViewClientHostApiCodec extends StandardMessageCodec { - public static final WebViewClientHostApiCodec INSTANCE = new WebViewClientHostApiCodec(); - private WebViewClientHostApiCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebViewClientHostApi { - void create(Long instanceId, Boolean shouldOverrideUrlLoading); + void create(@NonNull Long instanceId, @NonNull Boolean shouldOverrideUrlLoading); /** The codec used by WebViewClientHostApi. */ static MessageCodec getCodec() { - return WebViewClientHostApiCodec.INSTANCE; - } - - /** Sets up an instance of `WebViewClientHostApi` to handle messages through the `binaryMessenger`. */ + return new StandardMessageCodec(); } + /**Sets up an instance of `WebViewClientHostApi` to handle messages through the `binaryMessenger`. */ static void setup(BinaryMessenger binaryMessenger, WebViewClientHostApi api) { { BasicMessageChannel channel = @@ -1507,6 +1770,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewClientHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1515,7 +1779,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewClientHostApi api) { if (shouldOverrideUrlLoadingArg == null) { throw new NullPointerException("shouldOverrideUrlLoadingArg unexpectedly null."); } - api.create(instanceIdArg.longValue(), shouldOverrideUrlLoadingArg); + api.create((instanceIdArg == null) ? null : instanceIdArg.longValue(), shouldOverrideUrlLoadingArg); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1533,7 +1797,7 @@ private static class WebViewClientFlutterApiCodec extends StandardMessageCodec { public static final WebViewClientFlutterApiCodec INSTANCE = new WebViewClientFlutterApiCodec(); private WebViewClientFlutterApiCodec() {} @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { case (byte)128: return WebResourceErrorData.fromMap((Map) readValue(buffer)); @@ -1547,7 +1811,7 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { } } @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof WebResourceErrorData) { stream.write(128); writeValue(stream, ((WebResourceErrorData) value).toMap()); @@ -1562,7 +1826,7 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { } } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class WebViewClientFlutterApi { private final BinaryMessenger binaryMessenger; public WebViewClientFlutterApi(BinaryMessenger argBinaryMessenger){ @@ -1571,53 +1835,53 @@ public WebViewClientFlutterApi(BinaryMessenger argBinaryMessenger){ public interface Reply { void reply(T reply); } + /** The codec used by WebViewClientFlutterApi. */ static MessageCodec getCodec() { - return WebViewClientFlutterApiCodec.INSTANCE; + return WebViewClientFlutterApiCodec.INSTANCE; } - - public void dispose(Long instanceIdArg, Reply callback) { + public void dispose(@NonNull Long instanceIdArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.dispose", getCodec()); - channel.send(new ArrayList(Arrays.asList(instanceIdArg)), channelReply -> { + channel.send(new ArrayList(Collections.singletonList(instanceIdArg)), channelReply -> { callback.reply(null); }); } - public void onPageStarted(Long instanceIdArg, Long webViewInstanceIdArg, String urlArg, Reply callback) { + public void onPageStarted(@NonNull Long instanceIdArg, @NonNull Long webViewInstanceIdArg, @NonNull String urlArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted", getCodec()); channel.send(new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, urlArg)), channelReply -> { callback.reply(null); }); } - public void onPageFinished(Long instanceIdArg, Long webViewInstanceIdArg, String urlArg, Reply callback) { + public void onPageFinished(@NonNull Long instanceIdArg, @NonNull Long webViewInstanceIdArg, @NonNull String urlArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished", getCodec()); channel.send(new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, urlArg)), channelReply -> { callback.reply(null); }); } - public void onReceivedRequestError(Long instanceIdArg, Long webViewInstanceIdArg, WebResourceRequestData requestArg, WebResourceErrorData errorArg, Reply callback) { + public void onReceivedRequestError(@NonNull Long instanceIdArg, @NonNull Long webViewInstanceIdArg, @NonNull WebResourceRequestData requestArg, @NonNull WebResourceErrorData errorArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError", getCodec()); channel.send(new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, requestArg, errorArg)), channelReply -> { callback.reply(null); }); } - public void onReceivedError(Long instanceIdArg, Long webViewInstanceIdArg, Long errorCodeArg, String descriptionArg, String failingUrlArg, Reply callback) { + public void onReceivedError(@NonNull Long instanceIdArg, @NonNull Long webViewInstanceIdArg, @NonNull Long errorCodeArg, @NonNull String descriptionArg, @NonNull String failingUrlArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError", getCodec()); channel.send(new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, errorCodeArg, descriptionArg, failingUrlArg)), channelReply -> { callback.reply(null); }); } - public void requestLoading(Long instanceIdArg, Long webViewInstanceIdArg, WebResourceRequestData requestArg, Reply callback) { + public void requestLoading(@NonNull Long instanceIdArg, @NonNull Long webViewInstanceIdArg, @NonNull WebResourceRequestData requestArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading", getCodec()); channel.send(new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, requestArg)), channelReply -> { callback.reply(null); }); } - public void urlLoading(Long instanceIdArg, Long webViewInstanceIdArg, String urlArg, Reply callback) { + public void urlLoading(@NonNull Long instanceIdArg, @NonNull Long webViewInstanceIdArg, @NonNull String urlArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading", getCodec()); channel.send(new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, urlArg)), channelReply -> { @@ -1625,21 +1889,14 @@ public void urlLoading(Long instanceIdArg, Long webViewInstanceIdArg, String url }); } } - private static class DownloadListenerHostApiCodec extends StandardMessageCodec { - public static final DownloadListenerHostApiCodec INSTANCE = new DownloadListenerHostApiCodec(); - private DownloadListenerHostApiCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface DownloadListenerHostApi { - void create(Long instanceId); + void create(@NonNull Long instanceId); /** The codec used by DownloadListenerHostApi. */ static MessageCodec getCodec() { - return DownloadListenerHostApiCodec.INSTANCE; - } - - /** Sets up an instance of `DownloadListenerHostApi` to handle messages through the `binaryMessenger`. */ + return new StandardMessageCodec(); } + /**Sets up an instance of `DownloadListenerHostApi` to handle messages through the `binaryMessenger`. */ static void setup(BinaryMessenger binaryMessenger, DownloadListenerHostApi api) { { BasicMessageChannel channel = @@ -1649,11 +1906,12 @@ static void setup(BinaryMessenger binaryMessenger, DownloadListenerHostApi api) Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.create(instanceIdArg.longValue()); + api.create((instanceIdArg == null) ? null : instanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1667,12 +1925,7 @@ static void setup(BinaryMessenger binaryMessenger, DownloadListenerHostApi api) } } } - private static class DownloadListenerFlutterApiCodec extends StandardMessageCodec { - public static final DownloadListenerFlutterApiCodec INSTANCE = new DownloadListenerFlutterApiCodec(); - private DownloadListenerFlutterApiCodec() {} - } - - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class DownloadListenerFlutterApi { private final BinaryMessenger binaryMessenger; public DownloadListenerFlutterApi(BinaryMessenger argBinaryMessenger){ @@ -1681,18 +1934,18 @@ public DownloadListenerFlutterApi(BinaryMessenger argBinaryMessenger){ public interface Reply { void reply(T reply); } + /** The codec used by DownloadListenerFlutterApi. */ static MessageCodec getCodec() { - return DownloadListenerFlutterApiCodec.INSTANCE; + return new StandardMessageCodec(); } - - public void dispose(Long instanceIdArg, Reply callback) { + public void dispose(@NonNull Long instanceIdArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DownloadListenerFlutterApi.dispose", getCodec()); - channel.send(new ArrayList(Arrays.asList(instanceIdArg)), channelReply -> { + channel.send(new ArrayList(Collections.singletonList(instanceIdArg)), channelReply -> { callback.reply(null); }); } - public void onDownloadStart(Long instanceIdArg, String urlArg, String userAgentArg, String contentDispositionArg, String mimetypeArg, Long contentLengthArg, Reply callback) { + public void onDownloadStart(@NonNull Long instanceIdArg, @NonNull String urlArg, @NonNull String userAgentArg, @NonNull String contentDispositionArg, @NonNull String mimetypeArg, @NonNull Long contentLengthArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart", getCodec()); channel.send(new ArrayList(Arrays.asList(instanceIdArg, urlArg, userAgentArg, contentDispositionArg, mimetypeArg, contentLengthArg)), channelReply -> { @@ -1700,21 +1953,14 @@ public void onDownloadStart(Long instanceIdArg, String urlArg, String userAgentA }); } } - private static class WebChromeClientHostApiCodec extends StandardMessageCodec { - public static final WebChromeClientHostApiCodec INSTANCE = new WebChromeClientHostApiCodec(); - private WebChromeClientHostApiCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebChromeClientHostApi { - void create(Long instanceId, Long webViewClientInstanceId); + void create(@NonNull Long instanceId, @NonNull Long webViewClientInstanceId); /** The codec used by WebChromeClientHostApi. */ static MessageCodec getCodec() { - return WebChromeClientHostApiCodec.INSTANCE; - } - - /** Sets up an instance of `WebChromeClientHostApi` to handle messages through the `binaryMessenger`. */ + return new StandardMessageCodec(); } + /**Sets up an instance of `WebChromeClientHostApi` to handle messages through the `binaryMessenger`. */ static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { { BasicMessageChannel channel = @@ -1724,6 +1970,7 @@ static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; Number instanceIdArg = (Number)args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1732,7 +1979,7 @@ static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { if (webViewClientInstanceIdArg == null) { throw new NullPointerException("webViewClientInstanceIdArg unexpectedly null."); } - api.create(instanceIdArg.longValue(), webViewClientInstanceIdArg.longValue()); + api.create((instanceIdArg == null) ? null : instanceIdArg.longValue(), (webViewClientInstanceIdArg == null) ? null : webViewClientInstanceIdArg.longValue()); wrapped.put("result", null); } catch (Error | RuntimeException exception) { @@ -1746,22 +1993,15 @@ static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { } } } - private static class FlutterAssetManagerHostApiCodec extends StandardMessageCodec { - public static final FlutterAssetManagerHostApiCodec INSTANCE = new FlutterAssetManagerHostApiCodec(); - private FlutterAssetManagerHostApiCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface FlutterAssetManagerHostApi { - List list(String path); - String getAssetFilePathByName(String name); + @NonNull List list(@NonNull String path); + @NonNull String getAssetFilePathByName(@NonNull String name); /** The codec used by FlutterAssetManagerHostApi. */ static MessageCodec getCodec() { - return FlutterAssetManagerHostApiCodec.INSTANCE; - } - - /** Sets up an instance of `FlutterAssetManagerHostApi` to handle messages through the `binaryMessenger`. */ + return new StandardMessageCodec(); } + /**Sets up an instance of `FlutterAssetManagerHostApi` to handle messages through the `binaryMessenger`. */ static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi api) { { BasicMessageChannel channel = @@ -1771,6 +2011,7 @@ static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi ap Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; String pathArg = (String)args.get(0); if (pathArg == null) { throw new NullPointerException("pathArg unexpectedly null."); @@ -1795,6 +2036,7 @@ static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi ap Map wrapped = new HashMap<>(); try { ArrayList args = (ArrayList)message; + assert args != null; String nameArg = (String)args.get(0); if (nameArg == null) { throw new NullPointerException("nameArg unexpectedly null."); @@ -1813,12 +2055,7 @@ static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi ap } } } - private static class WebChromeClientFlutterApiCodec extends StandardMessageCodec { - public static final WebChromeClientFlutterApiCodec INSTANCE = new WebChromeClientFlutterApiCodec(); - private WebChromeClientFlutterApiCodec() {} - } - - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class WebChromeClientFlutterApi { private final BinaryMessenger binaryMessenger; public WebChromeClientFlutterApi(BinaryMessenger argBinaryMessenger){ @@ -1827,18 +2064,18 @@ public WebChromeClientFlutterApi(BinaryMessenger argBinaryMessenger){ public interface Reply { void reply(T reply); } + /** The codec used by WebChromeClientFlutterApi. */ static MessageCodec getCodec() { - return WebChromeClientFlutterApiCodec.INSTANCE; + return new StandardMessageCodec(); } - - public void dispose(Long instanceIdArg, Reply callback) { + public void dispose(@NonNull Long instanceIdArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.WebChromeClientFlutterApi.dispose", getCodec()); - channel.send(new ArrayList(Arrays.asList(instanceIdArg)), channelReply -> { + channel.send(new ArrayList(Collections.singletonList(instanceIdArg)), channelReply -> { callback.reply(null); }); } - public void onProgressChanged(Long instanceIdArg, Long webViewInstanceIdArg, Long progressArg, Reply callback) { + public void onProgressChanged(@NonNull Long instanceIdArg, @NonNull Long webViewInstanceIdArg, @NonNull Long progressArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged", getCodec()); channel.send(new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, progressArg)), channelReply -> { @@ -1846,11 +2083,73 @@ public void onProgressChanged(Long instanceIdArg, Long webViewInstanceIdArg, Lon }); } } - private static Map wrapError(Throwable exception) { + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface WebStorageHostApi { + void create(@NonNull Long instanceId); + void deleteAllData(@NonNull Long instanceId); + + /** The codec used by WebStorageHostApi. */ + static MessageCodec getCodec() { + return new StandardMessageCodec(); } + /**Sets up an instance of `WebStorageHostApi` to handle messages through the `binaryMessenger`. */ + static void setup(BinaryMessenger binaryMessenger, WebStorageHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.WebStorageHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + assert args != null; + Number instanceIdArg = (Number)args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + api.create((instanceIdArg == null) ? null : instanceIdArg.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.WebStorageHostApi.deleteAllData", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + assert args != null; + Number instanceIdArg = (Number)args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + api.deleteAllData((instanceIdArg == null) ? null : instanceIdArg.longValue()); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(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", null); + errorMap.put("details", "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); return errorMap; } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java index a368baf266dd..fefd577ee9b5 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java @@ -4,81 +4,205 @@ package io.flutter.plugins.webviewflutter; -import android.util.LongSparseArray; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.Nullable; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; import java.util.HashMap; -import java.util.Map; +import java.util.WeakHashMap; /** - * Maintains instances to intercommunicate with Dart objects. + * Maintains instances used to communicate with the corresponding objects in Dart. * - *

When an instance is added with an instanceId, either can be used to retrieve the other. + *

Objects stored in this container are represented by an object in Dart that is also stored in + * an InstanceManager with the same identifier. + * + *

When an instance is added with an identifier, either can be used to retrieve the other. + * + *

Added instances are added as a weak reference and a strong reference. When the strong + * reference is removed with `{@link #remove(long)}` and the weak reference is deallocated, the + * `finalizationListener` is made with the instance's identifier. However, if the strong reference + * is removed and then the identifier is retrieved with the intention to pass the identifier to Dart + * (e.g. calling {@link #getIdentifierForStrongReference(Object)}), the strong reference to the + * instance is recreated. The strong reference will then need to be removed manually again. */ +@SuppressWarnings("unchecked") public class InstanceManager { - private final LongSparseArray instanceIdsToInstances = new LongSparseArray<>(); - private final Map instancesToInstanceIds = new HashMap<>(); + // Identifiers are locked to a specific range to avoid collisions with objects + // created simultaneously from Dart. + // Host uses identifiers >= 2^16 and Dart is expected to use values n where, + // 0 <= n < 2^16. + private static final long MIN_HOST_CREATED_IDENTIFIER = 65536; + private static final long CLEAR_FINALIZED_WEAK_REFERENCES_INTERVAL = 30000; + + /** Interface for listening when a weak reference of an instance is removed from the manager. */ + public interface FinalizationListener { + void onFinalize(long identifier); + } + + private final WeakHashMap identifiers = new WeakHashMap<>(); + private final HashMap> weakInstances = new HashMap<>(); + private final HashMap strongInstances = new HashMap<>(); + + private final ReferenceQueue referenceQueue = new ReferenceQueue<>(); + private final HashMap, Long> weakReferencesToIdentifiers = new HashMap<>(); + + private final Handler handler = new Handler(Looper.getMainLooper()); + + private final FinalizationListener finalizationListener; + + private long nextIdentifier = MIN_HOST_CREATED_IDENTIFIER; + private boolean isClosed = false; /** - * Add a new instance to the manager. + * Instantiate a new manager. + * + *

When the manager is no longer needed, {@link #close()} must be called. * - *

If an instance or instanceId has already been added, it will be replaced by the new values. + * @param finalizationListener the listener for garbage collected weak references. + * @return a new `InstanceManager`. + */ + public static InstanceManager open(FinalizationListener finalizationListener) { + return new InstanceManager(finalizationListener); + } + + private InstanceManager(FinalizationListener finalizationListener) { + this.finalizationListener = finalizationListener; + handler.postDelayed( + this::releaseAllFinalizedInstances, CLEAR_FINALIZED_WEAK_REFERENCES_INTERVAL); + } + + /** + * Removes `identifier` and its associated strongly referenced instance, if present, from the + * manager. * - * @param instance the new object to be added - * @param instanceId unique id of the added object + * @param identifier the identifier paired to an instance. + * @param the expected return type. + * @return the removed instance if the manager contains the given identifier, otherwise null. */ - public void addInstance(Object instance, long instanceId) { - instancesToInstanceIds.put(instance, instanceId); - instanceIdsToInstances.append(instanceId, instance); + @Nullable + public T remove(long identifier) { + assertManagerIsNotClosed(); + return (T) strongInstances.remove(identifier); } /** - * Remove the instance with instanceId from the manager. + * Retrieves the identifier paired with an instance. + * + *

If the manager contains `instance`, as a strong or weak reference, the strong reference to + * `instance` will be recreated and will need to be removed again with {@link #remove(long)}. * - * @param instanceId the id of the instance to be removed - * @return the removed instance if the manager contains the instanceId, otherwise null + * @param instance an instance that may be stored in the manager. + * @return the identifier associated with `instance` if the manager contains the value, otherwise + * null. */ - public Object removeInstanceWithId(long instanceId) { - final Object instance = instanceIdsToInstances.get(instanceId); - if (instance != null) { - instanceIdsToInstances.remove(instanceId); - instancesToInstanceIds.remove(instance); + @Nullable + public Long getIdentifierForStrongReference(Object instance) { + assertManagerIsNotClosed(); + final Long identifier = identifiers.get(instance); + if (identifier != null) { + strongInstances.put(identifier, instance); } - return instance; + return identifier; } /** - * Remove the instance from the manager. + * Adds a new instance that was instantiated from Dart. * - * @param instance the instance to be removed - * @return the instanceId of the removed instance if the manager contains the value, otherwise - * null + *

If an instance or identifier has already been added, it will be replaced by the new values. + * The Dart InstanceManager is considered the source of truth and has the capability to overwrite + * stored pairs in response to hot restarts. + * + * @param instance the instance to be stored. + * @param identifier the identifier to be paired with instance. This value must be >= 0. */ - public Long removeInstance(Object instance) { - final Long instanceId = instancesToInstanceIds.get(instance); - if (instanceId != null) { - instanceIdsToInstances.remove(instanceId); - instancesToInstanceIds.remove(instance); + public void addDartCreatedInstance(Object instance, long identifier) { + assertManagerIsNotClosed(); + addInstance(instance, identifier); + } + + /** + * Adds a new instance that was instantiated from the host platform. + * + * @param instance the instance to be stored. + * @return the unique identifier stored with instance. + */ + public long addHostCreatedInstance(Object instance) { + assertManagerIsNotClosed(); + final long identifier = nextIdentifier++; + addInstance(instance, identifier); + return identifier; + } + + /** + * Retrieves the instance associated with identifier. + * + * @param identifier the identifier paired to an instance. + * @param the expected return type. + * @return the instance associated with `identifier` if the manager contains the value, otherwise + * null. + */ + @Nullable + public T getInstance(long identifier) { + assertManagerIsNotClosed(); + final WeakReference instance = (WeakReference) weakInstances.get(identifier); + if (instance != null) { + return instance.get(); } - return instanceId; + return (T) strongInstances.get(identifier); } /** - * Retrieve the Object paired with instanceId. + * Returns whether this manager contains the given `instance`. * - * @param instanceId the instanceId of the desired instance - * @return the instance stored with the instanceId if the manager contains the value, otherwise - * null + * @param instance the instance whose presence in this manager is to be tested. + * @return whether this manager contains the given `instance`. */ - public Object getInstance(long instanceId) { - return instanceIdsToInstances.get(instanceId); + public boolean containsInstance(Object instance) { + assertManagerIsNotClosed(); + return identifiers.containsKey(instance); } /** - * Retrieve the instanceId paired with an instance. + * Closes the manager and releases resources. * - * @param instance the value paired with the desired instanceId - * @return the instanceId paired with instance if the manager contains the value, otherwise null + *

Calling a method after calling this one will throw an {@link AssertionError}. This method + * excluded. */ - public Long getInstanceId(Object instance) { - return instancesToInstanceIds.get(instance); + public void close() { + handler.removeCallbacks(this::releaseAllFinalizedInstances); + isClosed = true; + } + + private void releaseAllFinalizedInstances() { + WeakReference reference; + while ((reference = (WeakReference) referenceQueue.poll()) != null) { + final Long identifier = weakReferencesToIdentifiers.remove(reference); + if (identifier != null) { + weakInstances.remove(identifier); + strongInstances.remove(identifier); + finalizationListener.onFinalize(identifier); + } + } + handler.postDelayed( + this::releaseAllFinalizedInstances, CLEAR_FINALIZED_WEAK_REFERENCES_INTERVAL); + } + + private void addInstance(Object instance, long identifier) { + if (identifier < 0) { + throw new IllegalArgumentException("Identifier must be >= 0."); + } + final WeakReference weakReference = new WeakReference<>(instance, referenceQueue); + identifiers.put(instance, identifier); + weakInstances.put(identifier, weakReference); + weakReferencesToIdentifiers.put(weakReference, identifier); + strongInstances.put(identifier, instance); + } + + private void assertManagerIsNotClosed() { + if (isClosed) { + throw new AssertionError("Manager has already been closed."); + } } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaObjectHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaObjectHostApiImpl.java new file mode 100644 index 000000000000..978e5232657d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaObjectHostApiImpl.java @@ -0,0 +1,32 @@ +// 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 androidx.annotation.NonNull; + +/** + * A pigeon Host API implementation that handles creating {@link Object}s and invoking its static + * and instance methods. + * + *

{@link Object} instances created by {@link JavaObjectHostApiImpl} are used to intercommunicate + * with a paired Dart object. + */ +public class JavaObjectHostApiImpl implements GeneratedAndroidWebView.JavaObjectHostApi { + private final InstanceManager instanceManager; + + /** + * Constructs a {@link JavaObjectHostApiImpl}. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + */ + public JavaObjectHostApiImpl(InstanceManager instanceManager) { + this.instanceManager = instanceManager; + } + + @Override + public void dispose(@NonNull Long identifier) { + instanceManager.remove(identifier); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java index 120f66dbdf9a..dbac83382a29 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java @@ -30,7 +30,7 @@ public JavaScriptChannelFlutterApiImpl( /** Passes arguments from {@link JavaScriptChannel#postMessage} to Dart. */ public void postMessage( JavaScriptChannel javaScriptChannel, String messageArg, Reply callback) { - super.postMessage(instanceManager.getInstanceId(javaScriptChannel), messageArg, callback); + super.postMessage(getIdentifierForJavaScriptChannel(javaScriptChannel), messageArg, callback); } /** @@ -40,11 +40,18 @@ public void postMessage( * @param callback Reply callback with return value from Dart. */ public void dispose(JavaScriptChannel javaScriptChannel, Reply callback) { - final Long instanceId = instanceManager.removeInstance(javaScriptChannel); - if (instanceId != null) { - dispose(instanceId, callback); + if (instanceManager.containsInstance(javaScriptChannel)) { + dispose(getIdentifierForJavaScriptChannel(javaScriptChannel), callback); } else { callback.reply(null); } } + + private long getIdentifierForJavaScriptChannel(JavaScriptChannel javaScriptChannel) { + final Long identifier = instanceManager.getIdentifierForStrongReference(javaScriptChannel); + if (identifier == null) { + throw new IllegalStateException("Could not find identifier for JavaScriptChannel."); + } + return identifier; + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java index 3055c9fc7f40..44e3b8aa5a2a 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java @@ -70,6 +70,6 @@ public void create(Long instanceId, String channelName) { final JavaScriptChannel javaScriptChannel = javaScriptChannelCreator.createJavaScriptChannel( flutterApi, channelName, platformThreadHandler); - instanceManager.addInstance(javaScriptChannel, instanceId); + instanceManager.addDartCreatedInstance(javaScriptChannel, instanceId); } } 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 2ab9275b41c3..28d63ec82dec 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 @@ -32,11 +32,12 @@ public WebChromeClientFlutterApiImpl( /** Passes arguments from {@link WebChromeClient#onProgressChanged} to Dart. */ public void onProgressChanged( WebChromeClient webChromeClient, WebView webView, Long progress, Reply callback) { + final Long webViewIdentifier = instanceManager.getIdentifierForStrongReference(webView); + if (webViewIdentifier == null) { + throw new IllegalStateException("Could not find identifier for WebView."); + } super.onProgressChanged( - instanceManager.getInstanceId(webChromeClient), - instanceManager.getInstanceId(webView), - progress, - callback); + getIdentifierForClient(webChromeClient), webViewIdentifier, progress, callback); } /** @@ -46,11 +47,18 @@ public void onProgressChanged( * @param callback reply callback with return value from Dart */ public void dispose(WebChromeClient webChromeClient, Reply callback) { - final Long instanceId = instanceManager.removeInstance(webChromeClient); - if (instanceId != null) { - dispose(instanceId, callback); + if (instanceManager.containsInstance(webChromeClient)) { + dispose(getIdentifierForClient(webChromeClient), callback); } else { callback.reply(null); } } + + private long getIdentifierForClient(WebChromeClient webChromeClient) { + final Long identifier = instanceManager.getIdentifierForStrongReference(webChromeClient); + if (identifier == null) { + throw new IllegalStateException("Could not find identifier for WebChromeClient."); + } + return identifier; + } } 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 c1a4e2c4b197..8e00659013e8 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 @@ -6,46 +6,43 @@ import static io.flutter.plugins.webviewflutter.WebViewFlutterPlugin.application; -import android.app.Application; +import android.Manifest; +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ClipData; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Build; import android.os.Message; +import android.provider.MediaStore; +import android.util.Log; import android.webkit.GeolocationPermissions; +import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.Size; import androidx.annotation.VisibleForTesting; -import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; -import android.net.Uri; -import android.util.Log; -import android.Manifest; -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.ClipData; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.hardware.display.DisplayManager; -import util.FileUtil; -import android.widget.Toast; -import android.content.ClipData; -import android.webkit.ValueCallback; -import android.provider.MediaStore; import java.io.File; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.Arrays; -import androidx.core.content.ContextCompat; -import androidx.core.content.FileProvider; -import androidx.core.app.ActivityCompat; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; +import util.FileUtil; /** * Host api implementation for {@link WebChromeClient}. @@ -227,7 +224,7 @@ public void create(Long instanceId, Long webViewClientInstanceId) { (WebViewClient) instanceManager.getInstance(webViewClientInstanceId); final WebChromeClient webChromeClient = webChromeClientCreator.createWebChromeClient(flutterApi, webViewClient); - instanceManager.addInstance(webChromeClient, instanceId); + instanceManager.addDartCreatedInstance(webChromeClient, instanceId); } private static void openImageChooserActivity() { @@ -250,6 +247,20 @@ private static void takePhotoOrOpenGallery() { if (WebViewFlutterPlugin.activity==null||!FileUtil.checkSDcard(WebViewFlutterPlugin.activity)) { return; } + String[] declaredPermissions = new String[0]; + try { + PackageInfo info = WebViewFlutterPlugin.activity.getPackageManager().getPackageInfo( + WebViewFlutterPlugin.activity.getPackageName(), + PackageManager.GET_PERMISSIONS + ); + declaredPermissions = info.requestedPermissions; + } catch (PackageManager.NameNotFoundException ignored) { + } + if (!Arrays.asList(declaredPermissions).contains(Manifest.permission.CAMERA)) { + openImageChooserActivity(); + return; + } + String[] selectPicTypeStr = {WebViewFlutterPlugin.activity.getString(R.string.take_photo), WebViewFlutterPlugin.activity.getString(R.string.photo_library)}; new AlertDialog.Builder(WebViewFlutterPlugin.activity, AlertDialog.THEME_DEVICE_DEFAULT_DARK) diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java index fe6da1590b69..b4ed6e40dc14 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java @@ -54,12 +54,13 @@ public WebSettingsHostApiImpl( @Override public void create(Long instanceId, Long webViewInstanceId) { final WebView webView = (WebView) instanceManager.getInstance(webViewInstanceId); - instanceManager.addInstance(webSettingsCreator.createWebSettings(webView), instanceId); + instanceManager.addDartCreatedInstance( + webSettingsCreator.createWebSettings(webView), instanceId); } @Override public void dispose(Long instanceId) { - instanceManager.removeInstanceWithId(instanceId); + instanceManager.remove(instanceId); } @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebStorageHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebStorageHostApiImpl.java new file mode 100644 index 000000000000..c06f2bc5796c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebStorageHostApiImpl.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.WebStorage; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebStorageHostApi; + +/** + * Host api implementation for {@link WebStorage}. + * + *

Handles creating {@link WebStorage}s that intercommunicate with a paired Dart object. + */ +public class WebStorageHostApiImpl implements WebStorageHostApi { + private final InstanceManager instanceManager; + private final WebStorageCreator webStorageCreator; + + /** Handles creating {@link WebStorage} for a {@link WebStorageHostApiImpl}. */ + public static class WebStorageCreator { + /** + * Creates a {@link WebStorage}. + * + * @return the created {@link WebStorage}. Defaults to {@link WebStorage#getInstance} + */ + public WebStorage createWebStorage() { + return WebStorage.getInstance(); + } + } + + /** + * Creates a host API that handles creating {@link WebStorage} and invoke its methods. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param webStorageCreator handles creating {@link WebStorage}s + */ + public WebStorageHostApiImpl( + InstanceManager instanceManager, WebStorageCreator webStorageCreator) { + this.instanceManager = instanceManager; + this.webStorageCreator = webStorageCreator; + } + + @Override + public void create(Long instanceId) { + instanceManager.addDartCreatedInstance(webStorageCreator.createWebStorage(), instanceId); + } + + @Override + public void deleteAllData(Long instanceId) { + final WebStorage webStorage = (WebStorage) instanceManager.getInstance(instanceId); + webStorage.deleteAllData(); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java index 9e462faa58a7..c23e8e79ae37 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java @@ -14,6 +14,7 @@ import androidx.webkit.WebResourceErrorCompat; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewClientFlutterApi; +import java.util.HashMap; /** * Flutter Api implementation for {@link WebViewClient}. @@ -26,40 +27,39 @@ public class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { @RequiresApi(api = Build.VERSION_CODES.M) static GeneratedAndroidWebView.WebResourceErrorData createWebResourceErrorData( WebResourceError error) { - final GeneratedAndroidWebView.WebResourceErrorData errorData = - new GeneratedAndroidWebView.WebResourceErrorData(); - errorData.setErrorCode((long) error.getErrorCode()); - errorData.setDescription(error.getDescription().toString()); - - return errorData; + return new GeneratedAndroidWebView.WebResourceErrorData.Builder() + .setErrorCode((long) error.getErrorCode()) + .setDescription(error.getDescription().toString()) + .build(); } @SuppressLint("RequiresFeature") static GeneratedAndroidWebView.WebResourceErrorData createWebResourceErrorData( WebResourceErrorCompat error) { - final GeneratedAndroidWebView.WebResourceErrorData errorData = - new GeneratedAndroidWebView.WebResourceErrorData(); - errorData.setErrorCode((long) error.getErrorCode()); - errorData.setDescription(error.getDescription().toString()); - - return errorData; + return new GeneratedAndroidWebView.WebResourceErrorData.Builder() + .setErrorCode((long) error.getErrorCode()) + .setDescription(error.getDescription().toString()) + .build(); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) static GeneratedAndroidWebView.WebResourceRequestData createWebResourceRequestData( WebResourceRequest request) { - final GeneratedAndroidWebView.WebResourceRequestData requestData = - new GeneratedAndroidWebView.WebResourceRequestData(); - requestData.setUrl(request.getUrl().toString()); - requestData.setIsForMainFrame(request.isForMainFrame()); + final GeneratedAndroidWebView.WebResourceRequestData.Builder requestData = + new GeneratedAndroidWebView.WebResourceRequestData.Builder() + .setUrl(request.getUrl().toString()) + .setIsForMainFrame(request.isForMainFrame()) + .setHasGesture(request.hasGesture()) + .setMethod(request.getMethod()) + .setRequestHeaders( + request.getRequestHeaders() != null + ? request.getRequestHeaders() + : new HashMap<>()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { requestData.setIsRedirect(request.isRedirect()); } - requestData.setHasGesture(request.hasGesture()); - requestData.setMethod(request.getMethod()); - requestData.setRequestHeaders(request.getRequestHeaders()); - return requestData; + return requestData.build(); } /** @@ -77,21 +77,21 @@ public WebViewClientFlutterApiImpl( /** Passes arguments from {@link WebViewClient#onPageStarted} to Dart. */ public void onPageStarted( WebViewClient webViewClient, WebView webView, String urlArg, Reply callback) { - onPageStarted( - instanceManager.getInstanceId(webViewClient), - instanceManager.getInstanceId(webView), - urlArg, - callback); + final Long webViewIdentifier = instanceManager.getIdentifierForStrongReference(webView); + if (webViewIdentifier == null) { + throw new IllegalStateException("Could not find identifier for WebView."); + } + onPageStarted(getIdentifierForClient(webViewClient), webViewIdentifier, urlArg, callback); } /** Passes arguments from {@link WebViewClient#onPageFinished} to Dart. */ public void onPageFinished( WebViewClient webViewClient, WebView webView, String urlArg, Reply callback) { - onPageFinished( - instanceManager.getInstanceId(webViewClient), - instanceManager.getInstanceId(webView), - urlArg, - callback); + final Long webViewIdentifier = instanceManager.getIdentifierForStrongReference(webView); + if (webViewIdentifier == null) { + throw new IllegalStateException("Could not find identifier for WebView."); + } + onPageFinished(getIdentifierForClient(webViewClient), webViewIdentifier, urlArg, callback); } /** @@ -105,9 +105,13 @@ public void onReceivedRequestError( WebResourceRequest request, WebResourceError error, Reply callback) { + final Long webViewIdentifier = instanceManager.getIdentifierForStrongReference(webView); + if (webViewIdentifier == null) { + throw new IllegalStateException("Could not find identifier for WebView."); + } onReceivedRequestError( - instanceManager.getInstanceId(webViewClient), - instanceManager.getInstanceId(webView), + getIdentifierForClient(webViewClient), + webViewIdentifier, createWebResourceRequestData(request), createWebResourceErrorData(error), callback); @@ -124,9 +128,13 @@ public void onReceivedRequestError( WebResourceRequest request, WebResourceErrorCompat error, Reply callback) { + final Long webViewIdentifier = instanceManager.getIdentifierForStrongReference(webView); + if (webViewIdentifier == null) { + throw new IllegalStateException("Could not find identifier for WebView."); + } onReceivedRequestError( - instanceManager.getInstanceId(webViewClient), - instanceManager.getInstanceId(webView), + getIdentifierForClient(webViewClient), + webViewIdentifier, createWebResourceRequestData(request), createWebResourceErrorData(error), callback); @@ -143,9 +151,13 @@ public void onReceivedError( String descriptionArg, String failingUrlArg, Reply callback) { + final Long webViewIdentifier = instanceManager.getIdentifierForStrongReference(webView); + if (webViewIdentifier == null) { + throw new IllegalStateException("Could not find identifier for WebView."); + } onReceivedError( - instanceManager.getInstanceId(webViewClient), - instanceManager.getInstanceId(webView), + getIdentifierForClient(webViewClient), + webViewIdentifier, errorCodeArg, descriptionArg, failingUrlArg, @@ -162,9 +174,13 @@ public void requestLoading( WebView webView, WebResourceRequest request, Reply callback) { + final Long webViewIdentifier = instanceManager.getIdentifierForStrongReference(webView); + if (webViewIdentifier == null) { + throw new IllegalStateException("Could not find identifier for WebView."); + } requestLoading( - instanceManager.getInstanceId(webViewClient), - instanceManager.getInstanceId(webView), + getIdentifierForClient(webViewClient), + webViewIdentifier, createWebResourceRequestData(request), callback); } @@ -174,11 +190,11 @@ public void requestLoading( */ public void urlLoading( WebViewClient webViewClient, WebView webView, String urlArg, Reply callback) { - urlLoading( - instanceManager.getInstanceId(webViewClient), - instanceManager.getInstanceId(webView), - urlArg, - callback); + final Long webViewIdentifier = instanceManager.getIdentifierForStrongReference(webView); + if (webViewIdentifier == null) { + throw new IllegalStateException("Could not find identifier for WebView."); + } + urlLoading(getIdentifierForClient(webViewClient), webViewIdentifier, urlArg, callback); } /** @@ -188,11 +204,18 @@ public void urlLoading( * @param callback reply callback with return value from Dart */ public void dispose(WebViewClient webViewClient, Reply callback) { - final Long instanceId = instanceManager.removeInstance(webViewClient); - if (instanceId != null) { - dispose(instanceId, callback); + if (instanceManager.containsInstance(webViewClient)) { + dispose(getIdentifierForClient(webViewClient), callback); } else { callback.reply(null); } } + + private long getIdentifierForClient(WebViewClient webViewClient) { + final Long identifier = instanceManager.getIdentifierForStrongReference(webViewClient); + if (identifier == null) { + throw new IllegalStateException("Could not find identifier for WebViewClient."); + } + return identifier; + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java index 6b659fae9c0f..4833ee917d34 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java @@ -244,6 +244,6 @@ public WebViewClientHostApiImpl( public void create(Long instanceId, Boolean shouldOverrideUrlLoading) { final WebViewClient webViewClient = webViewClientCreator.createWebViewClient(flutterApi, shouldOverrideUrlLoading); - instanceManager.addInstance(webViewClient, instanceId); + instanceManager.addDartCreatedInstance(webViewClient, instanceId); } } 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 726384508464..7b211b0c8821 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 @@ -11,6 +11,7 @@ import android.util.Log; import android.view.View; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.app.FlutterApplication; import io.flutter.embedding.engine.plugins.FlutterPlugin; @@ -24,6 +25,7 @@ import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebSettingsHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebStorageHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewClientHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewHostApi; @@ -43,6 +45,7 @@ *

Call {@link #registerWith} to use the stable {@code io.flutter.plugin.common} package instead. */ public class WebViewFlutterPlugin implements FlutterPlugin, ActivityAware/*, PluginRegistry.ActivityResultListener,PluginRegistry.RequestPermissionsResultListener*/ { + private InstanceManager instanceManager; private FlutterPluginBinding pluginBinding; private WebViewHostApiImpl webViewHostApi; private JavaScriptChannelHostApiImpl javaScriptChannelHostApi; @@ -91,7 +94,7 @@ private void setUp( View containerView, FlutterAssetManager flutterAssetManager) { - InstanceManager instanceManager = new InstanceManager(); + instanceManager = InstanceManager.open(identifier -> {}); viewRegistry.registerViewFactory( "plugins.flutter.io/webview", new FlutterWebViewFactory(instanceManager)); @@ -133,6 +136,9 @@ private void setUp( FlutterAssetManagerHostApi.setup( binaryMessenger, new FlutterAssetManagerHostApiImpl(flutterAssetManager)); CookieManagerHostApi.setup(binaryMessenger, new CookieManagerHostApiImpl()); + WebStorageHostApi.setup( + binaryMessenger, + new WebStorageHostApiImpl(instanceManager, new WebStorageHostApiImpl.WebStorageCreator())); } @Override @@ -152,6 +158,7 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + instanceManager.close(); } @Override @@ -205,24 +212,9 @@ private void updateContext(Context context) { javaScriptChannelHostApi.setPlatformThreadHandler(new Handler(context.getMainLooper())); } -// @Override -// public boolean onActivityResult(int requestCode, int resultCode, Intent data) { -// Log.v(TAG,"onActivityResult"); -// if (webChromeClientHostApi != null){ -// return webChromeClientHostApi.activityResult(requestCode, resultCode, data); -// } -// -// return false; -// } -// -// @Override -// public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { -// Log.v(TAG,"onRequestPermissionsResult"); -// if (webChromeClientHostApi != null){ -// return webChromeClientHostApi.requestPermissionsResult(requestCode, permissions, grantResults); -// } -// -// return false; -// } - + /** Maintains instances used to communicate with the corresponding objects in Dart. */ + @Nullable + public InstanceManager getInstanceManager() { + return instanceManager; + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java index 0f3161355dcb..778ad611d05f 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java @@ -21,6 +21,7 @@ import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.ReleasableWebViewClient; import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * Host api implementation for {@link WebView}. @@ -28,11 +29,6 @@ *

Handles creating {@link WebView}s that intercommunicate with a paired Dart object. */ public class WebViewHostApiImpl implements WebViewHostApi { - // TODO(bparrishMines): This can be removed once pigeon supports null values: https://github.com/flutter/flutter/issues/59118 - // Workaround to represent null Strings since pigeon doesn't support null - // values. - private static final String nullStringIdentifier = ""; - private final InstanceManager instanceManager; private final WebViewProxy webViewProxy; // Only used with WebView using virtual displays. @@ -340,7 +336,7 @@ public void create(Long instanceId, Boolean useHybridComposition) { : webViewProxy.createInputAwareWebView(context, containerView); displayListenerProxy.onPostWebViewInitialization(displayManager); - instanceManager.addInstance(webView, instanceId); + instanceManager.addDartCreatedInstance(webView, instanceId); } @Override @@ -348,15 +344,14 @@ public void dispose(Long instanceId) { final WebView instance = (WebView) instanceManager.getInstance(instanceId); if (instance != null) { ((Releasable) instance).release(); - instanceManager.removeInstance(instance); + instanceManager.remove(instanceId); } } @Override public void loadData(Long instanceId, String data, String mimeType, String encoding) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - webView.loadData( - data, parseNullStringIdentifier(mimeType), parseNullStringIdentifier(encoding)); + webView.loadData(data, mimeType, encoding); } @Override @@ -368,12 +363,7 @@ public void loadDataWithBaseUrl( String encoding, String historyUrl) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - webView.loadDataWithBaseURL( - parseNullStringIdentifier(baseUrl), - data, - parseNullStringIdentifier(mimeType), - parseNullStringIdentifier(encoding), - parseNullStringIdentifier(historyUrl)); + webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); } @Override @@ -391,8 +381,7 @@ public void postUrl(Long instanceId, String url, byte[] data) { @Override public String getUrl(Long instanceId) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - final String result = webView.getUrl(); - return result != null ? result : nullStringIdentifier; + return webView.getUrl(); } @Override @@ -441,8 +430,7 @@ public void evaluateJavascript( @Override public String getTitle(Long instanceId) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - final String result = webView.getTitle(); - return result != null ? result : nullStringIdentifier; + return webView.getTitle(); } @Override @@ -469,6 +457,16 @@ public Long getScrollY(Long instanceId) { return (long) webView.getScrollY(); } + @NonNull + @Override + public GeneratedAndroidWebView.WebViewPoint getScrollPosition(@NonNull Long instanceId) { + final WebView webView = Objects.requireNonNull(instanceManager.getInstance(instanceId)); + return new GeneratedAndroidWebView.WebViewPoint.Builder() + .setX((long) webView.getScrollX()) + .setY((long) webView.getScrollY()) + .build(); + } + @Override public void setWebContentsDebuggingEnabled(Boolean enabled) { webViewProxy.setWebContentsDebuggingEnabled(enabled); @@ -514,12 +512,8 @@ public void setBackgroundColor(Long instanceId, Long color) { webView.setBackgroundColor(color.intValue()); } - @Nullable - private static String parseNullStringIdentifier(String value) { - if (value.equals(nullStringIdentifier)) { - return null; - } - - return value; + /** Maintains instances used to communicate with the corresponding WebView Dart object. */ + public InstanceManager getInstanceManager() { + return instanceManager; } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java index 239119375e83..da25dace4517 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java @@ -13,6 +13,7 @@ import android.webkit.DownloadListener; import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerCreator; import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -31,7 +32,7 @@ public class DownloadListenerTest { @Before public void setUp() { - instanceManager = new InstanceManager(); + instanceManager = InstanceManager.open(identifier -> {}); final DownloadListenerCreator downloadListenerCreator = new DownloadListenerCreator() { @@ -48,6 +49,11 @@ public DownloadListenerImpl createDownloadListener( hostApiImpl.create(0L); } + @After + public void tearDown() { + instanceManager.close(); + } + @Test public void postMessage() { downloadListener.onDownloadStart( diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InstanceManagerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InstanceManagerTest.java new file mode 100644 index 000000000000..4731e2a4beb1 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InstanceManagerTest.java @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class InstanceManagerTest { + @Test + public void addDartCreatedInstance() { + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + + final Object object = new Object(); + instanceManager.addDartCreatedInstance(object, 0); + + assertEquals(object, instanceManager.getInstance(0)); + assertEquals((Long) 0L, instanceManager.getIdentifierForStrongReference(object)); + assertTrue(instanceManager.containsInstance(object)); + + instanceManager.close(); + } + + @Test + public void addHostCreatedInstance() { + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + + final Object object = new Object(); + long identifier = instanceManager.addHostCreatedInstance(object); + + assertNotNull(instanceManager.getInstance(identifier)); + assertEquals(object, instanceManager.getInstance(identifier)); + assertTrue(instanceManager.containsInstance(object)); + + instanceManager.close(); + } + + @Test + public void remove() { + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + + Object object = new Object(); + instanceManager.addDartCreatedInstance(object, 0); + + assertEquals(object, instanceManager.remove(0)); + + // To allow for object to be garbage collected. + //noinspection UnusedAssignment + object = null; + + Runtime.getRuntime().gc(); + + assertNull(instanceManager.getInstance(0)); + + instanceManager.close(); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaObjectHostApiTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaObjectHostApiTest.java new file mode 100644 index 000000000000..8ac349e76418 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaObjectHostApiTest.java @@ -0,0 +1,32 @@ +// 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.assertNull; + +import org.junit.Test; + +public class JavaObjectHostApiTest { + @Test + public void dispose() { + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + + final JavaObjectHostApiImpl hostApi = new JavaObjectHostApiImpl(instanceManager); + + Object object = new Object(); + instanceManager.addDartCreatedInstance(object, 0); + + // To free object for garbage collection. + //noinspection UnusedAssignment + object = null; + + hostApi.dispose(0L); + Runtime.getRuntime().gc(); + + assertNull(instanceManager.getInstance(0)); + + instanceManager.close(); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java index 3de81da81bec..4bde211c6a4d 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java @@ -12,6 +12,7 @@ import android.os.Handler; import io.flutter.plugins.webviewflutter.JavaScriptChannelHostApiImpl.JavaScriptChannelCreator; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -30,7 +31,7 @@ public class JavaScriptChannelTest { @Before public void setUp() { - instanceManager = new InstanceManager(); + instanceManager = InstanceManager.open(identifier -> {}); final JavaScriptChannelCreator javaScriptChannelCreator = new JavaScriptChannelCreator() { @@ -52,6 +53,11 @@ public JavaScriptChannel createJavaScriptChannel( hostApiImpl.create(0L, "aChannelName"); } + @After + public void tearDown() { + instanceManager.close(); + } + @Test public void postMessage() { javaScriptChannel.postMessage("A message post."); diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java index 63cd31043799..03d48d17df91 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java @@ -23,6 +23,7 @@ import android.webkit.WebViewClient; import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientCreator; import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -46,9 +47,10 @@ public class WebChromeClientTest { @Before public void setUp() { - instanceManager = new InstanceManager(); - instanceManager.addInstance(mockWebView, 0L); - instanceManager.addInstance(mockWebViewClient, 1L); + instanceManager = InstanceManager.open(identifier -> {}); + + instanceManager.addDartCreatedInstance(mockWebView, 0L); + instanceManager.addDartCreatedInstance(mockWebViewClient, 1L); final WebChromeClientCreator webChromeClientCreator = new WebChromeClientCreator() { @@ -65,6 +67,11 @@ public WebChromeClientImpl createWebChromeClient( hostApiImpl.create(2L, 1L); } + @After + public void tearDown() { + instanceManager.close(); + } + @Test public void onProgressChanged() { webChromeClient.onProgressChanged(mockWebView, 23); diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebSettingsTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebSettingsTest.java index 8ef32ddcb4ca..3217316ff563 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebSettingsTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebSettingsTest.java @@ -10,6 +10,7 @@ import android.webkit.WebSettings; import io.flutter.plugins.webviewflutter.WebSettingsHostApiImpl.WebSettingsCreator; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -29,12 +30,18 @@ public class WebSettingsTest { @Before public void setUp() { - testInstanceManager = new InstanceManager(); + testInstanceManager = InstanceManager.open(identifier -> {}); + when(mockWebSettingsCreator.createWebSettings(any())).thenReturn(mockWebSettings); testHostApiImpl = new WebSettingsHostApiImpl(testInstanceManager, mockWebSettingsCreator); testHostApiImpl.create(0L, 0L); } + @After + public void tearDown() { + testInstanceManager.close(); + } + @Test public void setDomStorageEnabled() { testHostApiImpl.setDomStorageEnabled(0L, true); diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebStorageHostApiImplTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebStorageHostApiImplTest.java new file mode 100644 index 000000000000..b4f38f1702de --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebStorageHostApiImplTest.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.webkit.WebStorage; +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 WebStorageHostApiImplTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public WebStorage mockWebStorage; + + @Mock WebStorageHostApiImpl.WebStorageCreator mockWebStorageCreator; + + InstanceManager testInstanceManager; + WebStorageHostApiImpl testHostApiImpl; + + @Before + public void setUp() { + testInstanceManager = InstanceManager.open(identifier -> {}); + + when(mockWebStorageCreator.createWebStorage()).thenReturn(mockWebStorage); + testHostApiImpl = new WebStorageHostApiImpl(testInstanceManager, mockWebStorageCreator); + testHostApiImpl.create(0L); + } + + @After + public void tearDown() { + testInstanceManager.close(); + } + + @Test + public void deleteAllData() { + testHostApiImpl.deleteAllData(0L); + verify(mockWebStorage).deleteAllData(); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java index 62d272366a6f..5d0cb701010e 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java @@ -4,16 +4,23 @@ 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.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.net.Uri; +import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientCompatImpl; import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientCreator; +import java.util.HashMap; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -34,8 +41,9 @@ public class WebViewClientTest { @Before public void setUp() { - instanceManager = new InstanceManager(); - instanceManager.addInstance(mockWebView, 0L); + instanceManager = InstanceManager.open(identifier -> {}); + + instanceManager.addDartCreatedInstance(mockWebView, 0L); final WebViewClientCreator webViewClientCreator = new WebViewClientCreator() { @@ -54,6 +62,11 @@ public WebViewClient createWebViewClient( hostApiImpl.create(1L, true); } + @After + public void tearDown() { + instanceManager.close(); + } + @Test public void onPageStarted() { webViewClient.onPageStarted(mockWebView, "https://www.google.com", null); @@ -96,4 +109,20 @@ public void urlLoading() { webViewClient.shouldOverrideUrlLoading(mockWebView, ""); verify(mockFlutterApi, never()).urlLoading((WebViewClient) any(), any(), any(), any()); } + + @Test + public void convertWebResourceRequestWithNullHeaders() { + final Uri mockUri = mock(Uri.class); + when(mockUri.toString()).thenReturn(""); + + final WebResourceRequest mockRequest = mock(WebResourceRequest.class); + when(mockRequest.getMethod()).thenReturn("method"); + when(mockRequest.getUrl()).thenReturn(mockUri); + when(mockRequest.isForMainFrame()).thenReturn(true); + when(mockRequest.getRequestHeaders()).thenReturn(null); + + final GeneratedAndroidWebView.WebResourceRequestData data = + WebViewClientFlutterApiImpl.createWebResourceRequestData(mockRequest); + assertEquals(data.getRequestHeaders(), new HashMap()); + } } 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/WebViewFlutterPluginTest.java new file mode 100644 index 000000000000..16dc6cf5de2b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFlutterPluginTest.java @@ -0,0 +1,45 @@ +// 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.assertNotNull; +import static org.mockito.Mockito.when; + +import android.content.Context; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.platform.PlatformViewRegistry; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class WebViewFlutterPluginTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock Context mockContext; + + @Mock BinaryMessenger mockBinaryMessenger; + + @Mock PlatformViewRegistry mockViewRegistry; + + @Mock FlutterPlugin.FlutterPluginBinding mockPluginBinding; + + @Test + public void getInstanceManagerAfterOnAttachedToEngine() { + final WebViewFlutterPlugin webViewFlutterPlugin = new WebViewFlutterPlugin(); + + when(mockPluginBinding.getApplicationContext()).thenReturn(mockContext); + when(mockPluginBinding.getPlatformViewRegistry()).thenReturn(mockViewRegistry); + when(mockPluginBinding.getBinaryMessenger()).thenReturn(mockBinaryMessenger); + + webViewFlutterPlugin.onAttachedToEngine(mockPluginBinding); + + assertNotNull(webViewFlutterPlugin.getInstanceManager()); + + webViewFlutterPlugin.onDetachedFromEngine(mockPluginBinding); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java index 2312b764342f..30bc256cd985 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java @@ -21,6 +21,7 @@ import io.flutter.plugins.webviewflutter.WebViewHostApiImpl.InputAwareWebViewPlatformView; import io.flutter.plugins.webviewflutter.WebViewHostApiImpl.WebViewPlatformView; import java.util.HashMap; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -43,13 +44,19 @@ public class WebViewTest { @Before public void setUp() { - testInstanceManager = new InstanceManager(); + testInstanceManager = InstanceManager.open(identifier -> {}); + when(mockWebViewProxy.createWebView(mockContext)).thenReturn(mockWebView); testHostApiImpl = new WebViewHostApiImpl(testInstanceManager, mockWebViewProxy, mockContext, null); testHostApiImpl.create(0L, true); } + @After + public void tearDown() { + testInstanceManager.close(); + } + @Test public void releaseWebView() { final WebViewPlatformView webView = new WebViewPlatformView(mockContext); @@ -166,8 +173,7 @@ public void loadData() { @Test public void loadDataWithNullValues() { - testHostApiImpl.loadData( - 0L, "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", "", ""); + testHostApiImpl.loadData(0L, "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", null, null); verify(mockWebView).loadData("VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", null, null); } @@ -192,12 +198,7 @@ public void loadDataWithBaseUrl() { @Test public void loadDataWithBaseUrlAndNullValues() { testHostApiImpl.loadDataWithBaseUrl( - 0L, - "", - "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", - "", - "", - ""); + 0L, null, "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", null, null, null); verify(mockWebView) .loadDataWithBaseURL(null, "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", null, null, null); } @@ -311,10 +312,19 @@ public void getScrollY() { assertEquals((long) testHostApiImpl.getScrollY(0L), 23); } + @Test + public void getScrollPosition() { + when(mockWebView.getScrollX()).thenReturn(1); + when(mockWebView.getScrollY()).thenReturn(2); + final GeneratedAndroidWebView.WebViewPoint position = testHostApiImpl.getScrollPosition(0L); + assertEquals((long) position.getX(), 1L); + assertEquals((long) position.getY(), 2L); + } + @Test public void setWebViewClient() { final WebViewClient mockWebViewClient = mock(WebViewClient.class); - testInstanceManager.addInstance(mockWebViewClient, 1L); + testInstanceManager.addDartCreatedInstance(mockWebViewClient, 1L); testHostApiImpl.setWebViewClient(0L, 1L); verify(mockWebView).setWebViewClient(mockWebViewClient); @@ -324,7 +334,7 @@ public void setWebViewClient() { public void addJavaScriptChannel() { final JavaScriptChannel javaScriptChannel = new JavaScriptChannel(mock(JavaScriptChannelFlutterApiImpl.class), "aName", null); - testInstanceManager.addInstance(javaScriptChannel, 1L); + testInstanceManager.addDartCreatedInstance(javaScriptChannel, 1L); testHostApiImpl.addJavaScriptChannel(0L, 1L); verify(mockWebView).addJavascriptInterface(javaScriptChannel, "aName"); @@ -334,7 +344,7 @@ public void addJavaScriptChannel() { public void removeJavaScriptChannel() { final JavaScriptChannel javaScriptChannel = new JavaScriptChannel(mock(JavaScriptChannelFlutterApiImpl.class), "aName", null); - testInstanceManager.addInstance(javaScriptChannel, 1L); + testInstanceManager.addDartCreatedInstance(javaScriptChannel, 1L); testHostApiImpl.removeJavaScriptChannel(0L, 1L); verify(mockWebView).removeJavascriptInterface("aName"); @@ -343,7 +353,7 @@ public void removeJavaScriptChannel() { @Test public void setDownloadListener() { final DownloadListener mockDownloadListener = mock(DownloadListener.class); - testInstanceManager.addInstance(mockDownloadListener, 1L); + testInstanceManager.addDartCreatedInstance(mockDownloadListener, 1L); testHostApiImpl.setDownloadListener(0L, 1L); verify(mockWebView).setDownloadListener(mockDownloadListener); @@ -352,7 +362,7 @@ public void setDownloadListener() { @Test public void setWebChromeClient() { final WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); - testInstanceManager.addInstance(mockWebChromeClient, 1L); + testInstanceManager.addDartCreatedInstance(mockWebChromeClient, 1L); testHostApiImpl.setWebChromeClient(0L, 1L); verify(mockWebView).setWebChromeClient(mockWebChromeClient); diff --git a/packages/webview_flutter/webview_flutter_android/example/README.md b/packages/webview_flutter/webview_flutter_android/example/README.md index 850ee74397a9..96b8bb17dbff 100644 --- a/packages/webview_flutter/webview_flutter_android/example/README.md +++ b/packages/webview_flutter/webview_flutter_android/example/README.md @@ -1,8 +1,9 @@ -# webview_flutter_example +# Platform Implementation Test App -Demonstrates how to use the webview_flutter plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle index fdd8193eb2b6..0c55b4d594be 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle +++ b/packages/webview_flutter/webview_flutter_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 32 lintOptions { disable 'InvalidPackage' @@ -55,8 +55,8 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - api 'androidx.test:core:1.2.0' + api 'androidx.test:core:1.4.0' } diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..29e413457635 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/webview_flutter/webview_flutter_android/example/android/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/build.gradle index d21451f63b46..ac477c248fc4 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/build.gradle +++ b/packages/webview_flutter/webview_flutter_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'com.android.tools.build:gradle:7.2.2' } } 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 a6738207fd15..94adc3a3f97a 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties +++ b/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -android.enableR8=true diff --git a/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties index ed1a787b3b22..cc5527d781a7 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/webview_flutter/webview_flutter_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-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 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 ac77187bd586..ece57272a97a 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 @@ -9,12 +9,13 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +// 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:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:webview_flutter_android_example/navigation_decision.dart'; @@ -27,8 +28,6 @@ import 'package:webview_pro_platform_interface/webview_flutter_platform_interfac Future main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - const bool _skipDueToIssue86757 = true; - final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0); server.forEach((HttpRequest request) { if (request.uri.path == '/hello.txt') { @@ -52,6 +51,7 @@ Future main() async { testWidgets('initialUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); + final Completer pageFinishedCompleter = Completer(); await tester.pumpWidget( MaterialApp( home: Directionality( @@ -62,19 +62,23 @@ Future main() async { onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, + onPageFinished: pageFinishedCompleter.complete, ), ), ), ); + final WebViewController controller = await controllerCompleter.future; + await pageFinishedCompleter.future; + final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); - }, skip: _skipDueToIssue86757); + }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('loadUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); + final StreamController pageLoads = StreamController(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -84,14 +88,20 @@ Future main() async { onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, + onPageFinished: (String url) { + pageLoads.add(url); + }, ), ), ); final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl(secondaryUrl); - final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, secondaryUrl); - }, skip: _skipDueToIssue86757); + await expectLater( + pageLoads.stream.firstWhere((String url) => url == secondaryUrl), + completion(secondaryUrl), + ); + }); testWidgets('evaluateJavascript', (WidgetTester tester) async { final Completer controllerCompleter = @@ -114,7 +124,6 @@ Future main() async { expect(result, equals('2')); }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -153,15 +162,14 @@ Future main() async { final String content = await controller .runJavascriptReturningResult('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); - }, skip: _skipDueToIssue86757); + }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('JavascriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer pageStarted = Completer(); final Completer pageLoaded = Completer(); - final List messagesReceived = []; + final Completer channelCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, @@ -178,7 +186,7 @@ Future main() async { JavascriptChannel( name: 'Echo', onMessageReceived: (JavascriptMessage message) { - messagesReceived.add(message.message); + channelCompleter.complete(message.message); }, ), }, @@ -195,10 +203,11 @@ Future main() async { await pageStarted.future; await pageLoaded.future; - expect(messagesReceived, isEmpty); + expect(channelCompleter.isCompleted, isFalse); await controller.runJavascript('Echo.postMessage("hello");'); - expect(messagesReceived, equals(['hello'])); - }, skip: _skipDueToIssue86757); + + await expectLater(channelCompleter.future, completion('hello')); + }); testWidgets('resize webview', (WidgetTester tester) async { final Completer initialResizeCompleter = Completer(); @@ -232,12 +241,12 @@ Future main() async { testWidgets('set custom userAgent', (WidgetTester tester) async { final Completer controllerCompleter1 = Completer(); - final GlobalKey _globalKey = GlobalKey(); + final GlobalKey globalKey = GlobalKey(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( - key: _globalKey, + key: globalKey, initialUrl: 'about:blank', javascriptMode: JavascriptMode.unrestricted, userAgent: 'Custom_User_Agent1', @@ -255,7 +264,7 @@ Future main() async { Directionality( textDirection: TextDirection.ltr, child: WebView( - key: _globalKey, + key: globalKey, initialUrl: 'about:blank', javascriptMode: JavascriptMode.unrestricted, userAgent: 'Custom_User_Agent2', @@ -267,18 +276,17 @@ Future main() async { expect(customUserAgent2, 'Custom_User_Agent2'); }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('use default platform userAgent after webView is rebuilt', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); - final GlobalKey _globalKey = GlobalKey(); + final GlobalKey globalKey = GlobalKey(); // Build the webView with no user agent to get the default platform user agent. await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( - key: _globalKey, + key: globalKey, initialUrl: primaryUrl, javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController controller) { @@ -294,7 +302,7 @@ Future main() async { Directionality( textDirection: TextDirection.ltr, child: WebView( - key: _globalKey, + key: globalKey, initialUrl: 'about:blank', javascriptMode: JavascriptMode.unrestricted, userAgent: 'Custom_User_Agent', @@ -308,7 +316,7 @@ Future main() async { Directionality( textDirection: TextDirection.ltr, child: WebView( - key: _globalKey, + key: globalKey, initialUrl: 'about:blank', javascriptMode: JavascriptMode.unrestricted, ), @@ -317,7 +325,7 @@ Future main() async { final String customUserAgent2 = await _getUserAgent(controller); expect(customUserAgent2, defaultPlatformUserAgent); - }, skip: _skipDueToIssue86757); + }); group('Video playback policy', () { late String videoTestBase64; @@ -405,8 +413,6 @@ Future main() async { onPageFinished: (String url) { pageLoaded.complete(null); }, - initialMediaPlaybackPolicy: - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, ), ), ); @@ -464,8 +470,6 @@ Future main() async { onPageFinished: (String url) { pageLoaded.complete(null); }, - initialMediaPlaybackPolicy: - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, ), ), ); @@ -615,8 +619,6 @@ Future main() async { onPageFinished: (String url) { pageLoaded.complete(null); }, - initialMediaPlaybackPolicy: - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, ), ), ); @@ -629,7 +631,7 @@ Future main() async { expect(isPaused, _webviewBool(true)); }); - testWidgets('Changes to initialMediaPlaybackPolocy are ignored', + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -684,8 +686,6 @@ Future main() async { onPageFinished: (String url) { pageLoaded.complete(null); }, - initialMediaPlaybackPolicy: - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, ), ), ); @@ -743,7 +743,6 @@ Future main() async { }); group('Programmatic Scroll', () { - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { const String scrollTestPage = ''' @@ -818,7 +817,7 @@ Future main() async { scrollPosY = await controller.getScrollY(); expect(scrollPosX, X_SCROLL * 2); expect(scrollPosY, Y_SCROLL * 2); - }, skip: _skipDueToIssue86757); + }); }); group('SurfaceAndroidWebView', () { @@ -830,7 +829,6 @@ Future main() async { WebView.platform = AndroidWebView(); }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { const String scrollTestPage = ''' @@ -897,9 +895,8 @@ Future main() async { scrollPosY = await controller.getScrollY(); expect(X_SCROLL * 2, scrollPosX); expect(Y_SCROLL * 2, scrollPosY); - }, skip: _skipDueToIssue86757); + }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('inputs are scrolled into view when focused', (WidgetTester tester) async { const String scrollTestPage = ''' @@ -1005,13 +1002,13 @@ Future main() async { lastInputClientRectRelativeToViewport['right'] <= viewportRectRelativeToViewport['right'], isTrue); - }, skip: _skipDueToIssue86757); + }); }); group('NavigationDelegate', () { const String blankPage = ''; - final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + - base64Encode(const Utf8Encoder().convert(blankPage)); + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + '${base64Encode(const Utf8Encoder().convert(blankPage))}'; testWidgets('can allow requests', (WidgetTester tester) async { final Completer controllerCompleter = @@ -1265,11 +1262,8 @@ Future main() async { await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); - }, - // Flaky on Android: https://github.com/flutter/flutter/issues/86757 - skip: _skipDueToIssue86757); + }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets( 'can open new window and go back', (WidgetTester tester) async { @@ -1305,9 +1299,8 @@ Future main() async { expect(controller.canGoBack(), completion(true)); await controller.goBack(); await pageLoaded.future; - expect(controller.currentUrl(), completion(primaryUrl)); + await expectLater(controller.currentUrl(), completion(primaryUrl)); }, - skip: _skipDueToIssue86757, ); testWidgets( @@ -1368,13 +1361,57 @@ Future main() async { final WebViewController controller = await controllerCompleter.future; await pageLoadCompleter.future; - expect(controller.runJavascriptReturningResult('iframeLoaded'), - completion('true')); - expect( - controller.runJavascriptReturningResult( - 'document.querySelector("p") && document.querySelector("p").textContent'), - completion('null'), + final String iframeLoaded = + await controller.runJavascriptReturningResult('iframeLoaded'); + expect(iframeLoaded, 'true'); + + final String elementText = await controller.runJavascriptReturningResult( + 'document.querySelector("p") && document.querySelector("p").textContent', + ); + expect(elementText, 'null'); + }, + ); + + testWidgets( + 'clearCache should clear local storage', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + + Completer pageLoadCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (_) => pageLoadCompleter.complete(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + + await pageLoadCompleter.future; + pageLoadCompleter = Completer(); + + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('localStorage.setItem("myCat", "Tom");'); + final String myCatItem = await controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', ); + expect(myCatItem, '"Tom"'); + + await controller.clearCache(); + await pageLoadCompleter.future; + + final String nullItem = await controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', + ); + expect(nullItem, 'null'); }, ); } @@ -1403,9 +1440,10 @@ Future _runJavaScriptReturningResult( class ResizableWebView extends StatefulWidget { const ResizableWebView({ + Key? key, required this.onResize, required this.onPageFinished, - }); + }) : super(key: key); final JavascriptMessageHandler onResize; final VoidCallback onPageFinished; 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 a1d14ab0059d..5a7f14a161e4 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -8,7 +8,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_driver/driver_extension.dart'; import 'package:path_provider/path_provider.dart'; @@ -57,8 +56,8 @@ const String kExamplePage = '''

Local demo page

- This is an example page used to demonstrate how to load a local file or HTML - string using the Flutter + This is an example page used to demonstrate how to load a local file or HTML + string using the Flutter webview plugin.

@@ -110,13 +109,10 @@ class _WebViewExampleState extends State<_WebViewExample> { _SampleMenu(_controller.future), ], ), - // We're using a Builder here so we have a context that is below the Scaffold - // to allow calling Scaffold.of(context) so we can show a snackbar. - body: Builder(builder: (BuildContext context) { - return WebView( - initialUrl: 'https:flutter.dev', - // initialUrl: 'https://www.weiyun.com/', - // initialUrl: 'https://www.wjx.cn/jq/27265670.aspx', + body: WebView( + //initialUrl: 'https:flutter.dev', + // initialUrl: 'https://www.weiyun.com/', + initialUrl: 'https://www.wjx.cn/jq/27265670.aspx', onWebViewCreated: (WebViewController controller) { _controller.complete(controller); }, @@ -141,8 +137,7 @@ class _WebViewExampleState extends State<_WebViewExample> { javascriptMode: JavascriptMode.unrestricted, userAgent: 'Custom_User_Agent', backgroundColor: const Color(0x80000000), - ); - }), + ), floatingActionButton: favoriteButton(), ); } @@ -156,8 +151,7 @@ class _WebViewExampleState extends State<_WebViewExample> { return FloatingActionButton( onPressed: () async { final String url = (await controller.data!.currentUrl())!; - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Favorited $url')), ); }, @@ -255,8 +249,8 @@ class _SampleMenu extends StatelessWidget { itemBuilder: (BuildContext context) => >[ PopupMenuItem<_MenuOptions>( value: _MenuOptions.showUserAgent, - child: const Text('Show user agent'), enabled: controller.hasData, + child: const Text('Show user agent'), ), const PopupMenuItem<_MenuOptions>( value: _MenuOptions.listCookies, @@ -325,8 +319,7 @@ class _SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { final String cookies = await controller.runJavascriptReturningResult('document.cookie'); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Column( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, @@ -342,8 +335,7 @@ class _SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { await controller.runJavascript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(const SnackBar( + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Added a test entry to cache.'), )); } @@ -351,6 +343,7 @@ class _SampleMenu extends StatelessWidget { Future _onListCache( WebViewController controller, BuildContext context) async { await controller.runJavascript('caches.keys()' + // ignore: missing_whitespace_between_adjacent_strings '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Snackbar.postMessage(caches))'); } @@ -358,8 +351,7 @@ class _SampleMenu extends StatelessWidget { Future _onClearCache( WebViewController controller, BuildContext context) async { await controller.clearCache(); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(const SnackBar( + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Cache cleared.'), )); } @@ -371,8 +363,7 @@ class _SampleMenu extends StatelessWidget { if (!hadCookies) { message = 'There are no cookies.'; } - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(message), )); } @@ -476,8 +467,7 @@ class _NavigationControls extends StatelessWidget { if (await controller!.canGoBack()) { await controller.goBack(); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No back history item')), ); return; @@ -492,8 +482,7 @@ class _NavigationControls extends StatelessWidget { if (await controller!.canGoForward()) { await controller.goForward(); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('No forward history item')), ); diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart index 566fd8174808..75622b6c2ef9 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart @@ -250,7 +250,7 @@ class WebView extends StatefulWidget { final Color? backgroundColor; @override - _WebViewState createState() => _WebViewState(); + State createState() => _WebViewState(); } class _WebViewState extends State { diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml index 48392fd6d3ec..2dff7a4b3daa 100644 --- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -8,7 +8,8 @@ environment: dependencies: flutter: sdk: flutter - + flutter_driver: + sdk: flutter path_provider: ^2.0.6 webview_pro_android: @@ -18,16 +19,14 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ + webview_flutter_platform_interface: ^1.8.0 dev_dependencies: - espresso: ^0.1.0+2 - flutter_driver: - sdk: flutter + espresso: ^0.2.0 flutter_test: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/webview_flutter/webview_flutter_android/generatePigeons.sh b/packages/webview_flutter/webview_flutter_android/generatePigeons.sh deleted file mode 100755 index 30a6918fc922..000000000000 --- a/packages/webview_flutter/webview_flutter_android/generatePigeons.sh +++ /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. - -flutter pub run pigeon \ ---input pigeons/android_webview.dart \ ---dart_out lib/src/android_webview.pigeon.dart \ ---dart_test_out test/android_webview.pigeon.dart \ ---java_out android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.Java \ ---java_package io.flutter.plugins.webviewflutter 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 11d7ff8790ca..8d77aa9a5e74 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 @@ -2,19 +2,58 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// TODO(bparrishMines): Replace unused callback methods in constructors with +// variables once automatic garbage collection is fully implemented. See +// https://github.com/flutter/flutter/issues/107199. +// ignore_for_file: avoid_unused_constructor_parameters + +// 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 'dart:ui'; 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_api_impls.dart'; +import 'instance_manager.dart'; + +/// Root of the Java class hierarchy. +/// +/// See https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html. +class JavaObject with Copyable { + /// Constructs a [JavaObject] without creating the associated Java object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + JavaObject.detached({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _api = JavaObjectHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + + /// Global instance of [InstanceManager]. + static final InstanceManager globalInstanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + /// Pigeon Host Api implementation for [JavaObject]. + final JavaObjectHostApiImpl _api; + + /// Release the reference to a native Java instance. + static void dispose(JavaObject instance) { + instance._api.instanceManager.removeWeakReference(instance); + } -// TODO(bparrishMines): This can be removed once pigeon supports null values: https://github.com/flutter/flutter/issues/59118 -// Workaround to represent null Strings since pigeon doesn't support null -// values. -const String _nullStringIdentifier = ''; + @override + JavaObject copy() { + return JavaObject.detached(); + } +} /// An Android View that displays web pages. /// @@ -35,12 +74,18 @@ const String _nullStringIdentifier = ''; /// [Web-based content](https://developer.android.com/guide/webapps). /// /// When a [WebView] is no longer needed [release] must be called. -class WebView { +class WebView extends JavaObject { /// Constructs a new WebView. - WebView({this.useHybridComposition = false}) { + WebView({this.useHybridComposition = false}) : super.detached() { api.createFromInstance(this); } + /// Constructs a [WebView] without creating the associated Java object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + WebView.detached({this.useHybridComposition = false}) : super.detached(); + /// Pigeon Host Api implementation for [WebView]. @visibleForTesting static WebViewHostApiImpl api = WebViewHostApiImpl(); @@ -102,8 +147,8 @@ class WebView { return api.loadDataFromInstance( this, data, - mimeType ?? _nullStringIdentifier, - encoding ?? _nullStringIdentifier, + mimeType, + encoding, ); } @@ -151,11 +196,11 @@ class WebView { }) { return api.loadDataWithBaseUrlFromInstance( this, - baseUrl ?? _nullStringIdentifier, + baseUrl, data, - mimeType ?? _nullStringIdentifier, - encoding ?? _nullStringIdentifier, - historyUrl ?? _nullStringIdentifier, + mimeType, + encoding, + historyUrl, ); } @@ -184,12 +229,8 @@ class WebView { /// begun, the current page may not have changed. /// /// Returns null if no page has been loaded. - Future getUrl() async { - final String result = await api.getUrlFromInstance(this); - if (result == _nullStringIdentifier) { - return null; - } - return result; + Future getUrl() { + return api.getUrlFromInstance(this); } /// Whether this WebView has a back history item. @@ -235,27 +276,19 @@ class WebView { /// JavaScript state from an empty WebView is no longer persisted across /// navigations like [loadUrl]. For example, global variables and functions /// defined before calling [loadUrl]) will not exist in the loaded page. - Future evaluateJavascript(String javascriptString) async { - final String result = await api.evaluateJavascriptFromInstance( + Future evaluateJavascript(String javascriptString) { + return api.evaluateJavascriptFromInstance( this, javascriptString, ); - if (result == _nullStringIdentifier) { - return null; - } - return result; } // TODO(bparrishMines): Update documentation when WebViewClient.onReceivedTitle is added. /// Gets the title for the current page. /// /// Returns null if no page has been loaded. - Future getTitle() async { - final String result = await api.getTitleFromInstance(this); - if (result == _nullStringIdentifier) { - return null; - } - return result; + Future getTitle() { + return api.getTitleFromInstance(this); } // TODO(bparrishMines): Update documentation when onScrollChanged is added. @@ -288,6 +321,11 @@ class WebView { return api.getScrollYFromInstance(this); } + /// Returns the X and Y scroll position of this view. + Future getScrollPosition() { + return api.getScrollPositionFromInstance(this); + } + /// Sets the [WebViewClient] that will receive various notifications and requests. /// /// This will replace the current handler. @@ -337,9 +375,11 @@ class WebView { /// Registers the interface to be used when content can not be handled by the rendering engine, and should be downloaded instead. /// /// This will replace the current handler. - Future setDownloadListener(DownloadListener listener) { - DownloadListener.api.createFromInstance(listener); - return api.setDownloadListenerFromInstance(this, listener); + Future setDownloadListener(DownloadListener? listener) async { + await Future.wait(>[ + if (listener != null) DownloadListener.api.createFromInstance(listener), + api.setDownloadListenerFromInstance(this, listener) + ]); } /// Sets the chrome handler. @@ -347,7 +387,7 @@ class WebView { /// This is an implementation of [WebChromeClient] for use in handling /// JavaScript dialogs, favicons, titles, and the progress. This will replace /// the current handler. - Future setWebChromeClient(WebChromeClient client) { + Future setWebChromeClient(WebChromeClient? client) async { // WebView requires a WebViewClient because of a bug fix that makes // calls to WebViewClient.requestLoading/WebViewClient.urlLoading when a new // window is opened. This is to make sure a url opened by `Window.open` has @@ -356,8 +396,11 @@ class WebView { _currentWebViewClient != null, "Can't set a WebChromeClient without setting a WebViewClient first.", ); - WebChromeClient.api.createFromInstance(client, _currentWebViewClient!); - return api.setWebChromeClientFromInstance(this, client); + await Future.wait(>[ + if (client != null) + WebChromeClient.api.createFromInstance(client, _currentWebViewClient!), + api.setWebChromeClientFromInstance(this, client), + ]); } /// Sets the background color of this WebView. @@ -373,6 +416,11 @@ class WebView { WebSettings.api.disposeFromInstance(settings); return api.disposeFromInstance(this); } + + @override + WebView copy() { + return WebView.detached(useHybridComposition: useHybridComposition); + } } /// Manages cookies globally for all webviews. @@ -425,16 +473,22 @@ class CookieManager { /// obtained from [WebView.settings] is tied to the life of the WebView. If a /// WebView has been destroyed, any method call on [WebSettings] will throw an /// Exception. -class WebSettings { +class WebSettings extends JavaObject { /// Constructs a [WebSettings]. /// /// This constructor is only used for testing. An instance should be obtained /// with [WebView.settings]. @visibleForTesting - WebSettings(WebView webView) { + WebSettings(WebView webView) : super.detached() { api.createFromInstance(this, webView); } + /// Constructs a [WebSettings] without creating the associated Java object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + WebSettings.detached() : super.detached(); + /// Pigeon Host Api implementation for [WebSettings]. @visibleForTesting static WebSettingsHostApiImpl api = WebSettingsHostApiImpl(); @@ -477,7 +531,7 @@ class WebSettings { /// If the string is empty, the system default value will be used. Note that /// starting from KITKAT Android version, changing the user-agent while /// loading a web page causes WebView to initiate loading once again. - Future setUserAgentString(String userAgentString) { + Future setUserAgentString(String? userAgentString) { return api.setUserAgentStringFromInstance(this, userAgentString); } @@ -563,17 +617,35 @@ class WebSettings { Future setAllowFileAccess(bool enabled) { return api.setAllowFileAccessFromInstance(this, enabled); } + + @override + WebSettings copy() { + return WebSettings.detached(); + } } /// Exposes a channel to receive calls from javaScript. /// /// See [WebView.addJavaScriptChannel]. -abstract class JavaScriptChannel { +class JavaScriptChannel extends JavaObject { /// Constructs a [JavaScriptChannel]. - JavaScriptChannel(this.channelName) { + JavaScriptChannel( + this.channelName, { + void Function(String message)? postMessage, + }) : super.detached() { AndroidWebViewFlutterApis.instance.ensureSetUp(); } + /// Constructs a [JavaScriptChannel] without creating the associated Java + /// object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + JavaScriptChannel.detached( + this.channelName, { + void Function(String message)? postMessage, + }) : super.detached(); + /// Pigeon Host Api implementation for [JavaScriptChannel]. @visibleForTesting static JavaScriptChannelHostApiImpl api = JavaScriptChannelHostApiImpl(); @@ -582,16 +654,65 @@ abstract class JavaScriptChannel { final String channelName; /// Callback method when javaScript calls `postMessage` on the object instance passed. - void postMessage(String message); + void postMessage(String message) {} + + @override + JavaScriptChannel copy() { + return JavaScriptChannel.detached(channelName, postMessage: postMessage); + } } /// Receive various notifications and requests for [WebView]. -abstract class WebViewClient { +class WebViewClient extends JavaObject { /// Constructs a [WebViewClient]. - WebViewClient({this.shouldOverrideUrlLoading = true}) { + WebViewClient({ + this.shouldOverrideUrlLoading = true, + void Function(WebView webView, String url)? onPageStarted, + void Function(WebView webView, String url)? onPageFinished, + void Function( + WebView webView, + WebResourceRequest request, + WebResourceError error, + )? + onReceivedRequestError, + void Function( + WebView webView, + int errorCode, + String description, + String failingUrl, + )? + onReceivedError, + void Function(WebView webView, WebResourceRequest request)? requestLoading, + void Function(WebView webView, String url)? urlLoading, + }) : super.detached() { AndroidWebViewFlutterApis.instance.ensureSetUp(); } + /// Constructs a [WebViewClient] without creating the associated Java object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + WebViewClient.detached({ + this.shouldOverrideUrlLoading = true, + void Function(WebView webView, String url)? onPageStarted, + void Function(WebView webView, String url)? onPageFinished, + void Function( + WebView webView, + WebResourceRequest request, + WebResourceError error, + )? + onReceivedRequestError, + void Function( + WebView webView, + int errorCode, + String description, + String failingUrl, + )? + onReceivedError, + void Function(WebView webView, WebResourceRequest request)? requestLoading, + void Function(WebView webView, String url)? urlLoading, + }) : super.detached(); + /// User authentication failed on server. /// /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_AUTHENTICATION @@ -752,15 +873,54 @@ abstract class WebViewClient { /// causes the current [WebView] to abort loading the URL, while returning /// false causes the [WebView] to continue loading the URL as usual. void urlLoading(WebView webView, String url) {} + + @override + WebViewClient copy() { + return WebViewClient.detached( + shouldOverrideUrlLoading: shouldOverrideUrlLoading, + onPageStarted: onPageStarted, + onPageFinished: onPageFinished, + onReceivedRequestError: onReceivedRequestError, + onReceivedError: onReceivedError, + requestLoading: requestLoading, + urlLoading: urlLoading, + ); + } } -/// The interface to be used when content can not be handled by the rendering engine for [WebView], and should be downloaded instead. -abstract class DownloadListener { +/// The interface to be used when content can not be handled by the rendering +/// engine for [WebView], and should be downloaded instead. +class DownloadListener extends JavaObject { /// Constructs a [DownloadListener]. - DownloadListener() { + DownloadListener({ + void Function( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + )? + onDownloadStart, + }) : super.detached() { AndroidWebViewFlutterApis.instance.ensureSetUp(); } + /// Constructs a [DownloadListener] without creating the associated Java + /// object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + DownloadListener.detached({ + void Function( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + )? + onDownloadStart, + }) : super.detached(); + /// Pigeon Host Api implementation for [DownloadListener]. @visibleForTesting static DownloadListenerHostApiImpl api = DownloadListenerHostApiImpl(); @@ -772,22 +932,43 @@ abstract class DownloadListener { String contentDisposition, String mimetype, int contentLength, - ); + ) {} + + @override + DownloadListener copy() { + return DownloadListener(onDownloadStart: onDownloadStart); + } } /// Handles JavaScript dialogs, favicons, titles, and the progress for [WebView]. -abstract class WebChromeClient { +class WebChromeClient extends JavaObject { /// Constructs a [WebChromeClient]. - WebChromeClient() { + WebChromeClient({ + void Function(WebView webView, int progress)? onProgressChanged, + }) : super.detached() { AndroidWebViewFlutterApis.instance.ensureSetUp(); } + /// Constructs a [WebChromeClient] without creating the associated Java + /// object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + WebChromeClient.detached({ + void Function(WebView webView, int progress)? onProgressChanged, + }) : super.detached(); + /// Pigeon Host Api implementation for [WebChromeClient]. @visibleForTesting static WebChromeClientHostApiImpl api = WebChromeClientHostApiImpl(); /// Notify the host application that a file should be downloaded. void onProgressChanged(WebView webView, int progress) {} + + @override + WebChromeClient copy() { + return WebChromeClient.detached(onProgressChanged: onProgressChanged); + } } /// Encompasses parameters to the [WebViewClient.requestLoading] method. @@ -859,3 +1040,41 @@ class FlutterAssetManager { Future getAssetFilePathByName(String name) => api.getAssetFilePathByName(name); } + +/// Manages the JavaScript storage APIs provided by the [WebView]. +/// +/// Wraps [WebStorage](https://developer.android.com/reference/android/webkit/WebStorage). +class WebStorage extends JavaObject { + /// Constructs a [WebStorage]. + /// + /// This constructor is only used for testing. An instance should be obtained + /// with [WebStorage.instance]. + @visibleForTesting + WebStorage() : super.detached() { + AndroidWebViewFlutterApis.instance.ensureSetUp(); + api.createFromInstance(this); + } + + /// Constructs a [WebStorage] without creating the associated Java object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + WebStorage.detached() : super.detached(); + + /// Pigeon Host Api implementation for [WebStorage]. + @visibleForTesting + static WebStorageHostApiImpl api = WebStorageHostApiImpl(); + + /// The singleton instance of this class. + static WebStorage instance = WebStorage(); + + /// Clears all storage currently being used by the JavaScript storage APIs. + Future deleteAllData() { + return api.deleteAllDataFromInstance(this); + } + + @override + WebStorage copy() { + return WebStorage.detached(); + } +} 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.pigeon.dart index 394eacd40c66..a16531c18e29 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.pigeon.dart @@ -1,20 +1,31 @@ -// Autogenerated from Pigeon (v1.0.9), do not edit directly. +// 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. // 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 -// @dart = 2.12 +// 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 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; class WebResourceRequestData { - String? url; - bool? isForMainFrame; + WebResourceRequestData({ + required this.url, + required this.isForMainFrame, + this.isRedirect, + required this.hasGesture, + required this.method, + required this.requestHeaders, + }); + + String url; + bool isForMainFrame; bool? isRedirect; - bool? hasGesture; - String? method; - Map? requestHeaders; + bool hasGesture; + String method; + Map requestHeaders; Object encode() { final Map pigeonMap = {}; @@ -29,20 +40,25 @@ class WebResourceRequestData { static WebResourceRequestData decode(Object message) { final Map pigeonMap = message as Map; - 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(); + 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(), + ); } } class WebResourceErrorData { - int? errorCode; - String? description; + WebResourceErrorData({ + required this.errorCode, + required this.description, + }); + + int errorCode; + String description; Object encode() { final Map pigeonMap = {}; @@ -53,47 +69,135 @@ class WebResourceErrorData { static WebResourceErrorData decode(Object message) { final Map pigeonMap = message as Map; - return WebResourceErrorData() - ..errorCode = pigeonMap['errorCode'] as int? - ..description = pigeonMap['description'] as String?; + return WebResourceErrorData( + errorCode: pigeonMap['errorCode']! as int, + description: pigeonMap['description']! as String, + ); + } +} + +class WebViewPoint { + WebViewPoint({ + required this.x, + required this.y, + }); + + int x; + int y; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['x'] = x; + pigeonMap['y'] = y; + return pigeonMap; + } + + static WebViewPoint decode(Object message) { + final Map pigeonMap = message as Map; + return WebViewPoint( + x: pigeonMap['x']! as int, + y: pigeonMap['y']! as int, + ); + } +} + + +/// Handles methods calls to the native Java Object class. +/// +/// Also handles calls to remove the reference to an instance with `dispose`. +/// +/// See https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html. +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 = StandardMessageCodec(); + + 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 _CookieManagerHostApiCodec extends StandardMessageCodec { - const _CookieManagerHostApiCodec(); +/// Handles callbacks methods for the native Java Object class. +/// +/// See https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html. +abstract class JavaObjectFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + 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 CookieManagerHostApi { /// Constructor for [CookieManagerHostApi]. 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. - CookieManagerHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - + CookieManagerHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _CookieManagerHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future clearCookies() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CookieManagerHostApi.clearCookies', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.CookieManagerHostApi.clearCookies', 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.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + 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 bool?)!; } @@ -101,19 +205,16 @@ class CookieManagerHostApi { Future setCookie(String arg_url, String arg_value) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CookieManagerHostApi.setCookie', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_url, arg_value]) as Map?; + 'dev.flutter.pigeon.CookieManagerHostApi.setCookie', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_url, arg_value]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -125,37 +226,52 @@ class CookieManagerHostApi { } } -class _WebViewHostApiCodec extends StandardMessageCodec { +class _WebViewHostApiCodec extends StandardMessageCodec{ const _WebViewHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WebViewPoint) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else +{ + super.writeValue(buffer, value); + } + } + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WebViewPoint.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + + } + } } class WebViewHostApi { /// Constructor for [WebViewHostApi]. 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. - WebViewHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - + WebViewHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _WebViewHostApiCodec(); Future create(int arg_instanceId, bool arg_useHybridComposition) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.create', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.create', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_useHybridComposition]) - as Map?; + await channel.send([arg_instanceId, arg_useHybridComposition]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -168,19 +284,16 @@ class WebViewHostApi { Future dispose(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.dispose', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.dispose', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -191,23 +304,18 @@ class WebViewHostApi { } } - Future loadData(int arg_instanceId, String arg_data, - String arg_mimeType, String arg_encoding) async { + Future loadData(int arg_instanceId, String arg_data, String? arg_mimeType, String? arg_encoding) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.loadData', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel.send( - [arg_instanceId, arg_data, arg_mimeType, arg_encoding]) - as Map?; + 'dev.flutter.pigeon.WebViewHostApi.loadData', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_data, arg_mimeType, arg_encoding]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -218,33 +326,18 @@ class WebViewHostApi { } } - Future loadDataWithBaseUrl( - int arg_instanceId, - String arg_baseUrl, - String arg_data, - String arg_mimeType, - String arg_encoding, - String arg_historyUrl) async { + Future loadDataWithBaseUrl(int arg_instanceId, String? arg_baseUrl, String arg_data, String? arg_mimeType, String? arg_encoding, String? arg_historyUrl) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel.send([ - arg_instanceId, - arg_baseUrl, - arg_data, - arg_mimeType, - arg_encoding, - arg_historyUrl - ]) as Map?; + 'dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_baseUrl, arg_data, arg_mimeType, arg_encoding, arg_historyUrl]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -255,23 +348,18 @@ class WebViewHostApi { } } - Future loadUrl(int arg_instanceId, String arg_url, - Map arg_headers) async { + Future loadUrl(int arg_instanceId, String arg_url, Map arg_headers) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.loadUrl', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.loadUrl', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_url, arg_headers]) - as Map?; + await channel.send([arg_instanceId, arg_url, arg_headers]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -282,23 +370,18 @@ class WebViewHostApi { } } - Future postUrl( - int arg_instanceId, String arg_url, Uint8List arg_data) async { + Future postUrl(int arg_instanceId, String arg_url, Uint8List arg_data) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.postUrl', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.postUrl', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_url, arg_data]) - as Map?; + await channel.send([arg_instanceId, arg_url, arg_data]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -309,51 +392,50 @@ class WebViewHostApi { } } - Future getUrl(int arg_instanceId) async { + Future getUrl(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.getUrl', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.getUrl', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + 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 String?)!; + return (replyMap['result'] as String?); } } Future canGoBack(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.canGoBack', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.canGoBack', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + 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 bool?)!; } @@ -361,24 +443,26 @@ class WebViewHostApi { Future canGoForward(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.canGoForward', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.canGoForward', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + 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 bool?)!; } @@ -386,19 +470,16 @@ class WebViewHostApi { Future goBack(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.goBack', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.goBack', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -411,19 +492,16 @@ class WebViewHostApi { Future goForward(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.goForward', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.goForward', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -436,19 +514,16 @@ class WebViewHostApi { Future reload(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.reload', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.reload', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -461,20 +536,16 @@ class WebViewHostApi { Future clearCache(int arg_instanceId, bool arg_includeDiskFiles) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.clearCache', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.clearCache', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_includeDiskFiles]) - as Map?; + await channel.send([arg_instanceId, arg_includeDiskFiles]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -485,73 +556,62 @@ class WebViewHostApi { } } - Future evaluateJavascript( - int arg_instanceId, String arg_javascriptString) async { + Future evaluateJavascript(int arg_instanceId, String arg_javascriptString) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.evaluateJavascript', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.evaluateJavascript', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_javascriptString]) - as Map?; + await channel.send([arg_instanceId, arg_javascriptString]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + 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 String?)!; + return (replyMap['result'] as String?); } } - Future getTitle(int arg_instanceId) async { + Future getTitle(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.getTitle', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.getTitle', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + 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 String?)!; + return (replyMap['result'] as String?); } } Future scrollTo(int arg_instanceId, int arg_x, int arg_y) async { 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?; + '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) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -564,19 +624,16 @@ class WebViewHostApi { Future scrollBy(int arg_instanceId, int arg_x, int arg_y) async { 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?; + '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) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -589,24 +646,26 @@ class WebViewHostApi { Future getScrollX(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.getScrollX', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.getScrollX', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + 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?)!; } @@ -614,45 +673,70 @@ class WebViewHostApi { Future getScrollY(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.getScrollY', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.getScrollY', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + 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 getScrollPosition(int arg_instanceId) async { + 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) { + 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 WebViewPoint?)!; + } + } + Future setWebContentsDebuggingEnabled(bool arg_enabled) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled', - codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_enabled]) as Map?; + await channel.send([arg_enabled]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -663,23 +747,18 @@ class WebViewHostApi { } } - Future setWebViewClient( - int arg_instanceId, int arg_webViewClientInstanceId) async { + Future setWebViewClient(int arg_instanceId, int arg_webViewClientInstanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.setWebViewClient', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_webViewClientInstanceId]) - as Map?; + 'dev.flutter.pigeon.WebViewHostApi.setWebViewClient', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_webViewClientInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -690,23 +769,18 @@ class WebViewHostApi { } } - Future addJavaScriptChannel( - int arg_instanceId, int arg_javaScriptChannelInstanceId) async { + Future addJavaScriptChannel(int arg_instanceId, int arg_javaScriptChannelInstanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_javaScriptChannelInstanceId]) - as Map?; + 'dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_javaScriptChannelInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -717,23 +791,18 @@ class WebViewHostApi { } } - Future removeJavaScriptChannel( - int arg_instanceId, int arg_javaScriptChannelInstanceId) async { + Future removeJavaScriptChannel(int arg_instanceId, int arg_javaScriptChannelInstanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_javaScriptChannelInstanceId]) - as Map?; + 'dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_javaScriptChannelInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -744,23 +813,18 @@ class WebViewHostApi { } } - Future setDownloadListener( - int arg_instanceId, int arg_listenerInstanceId) async { + Future setDownloadListener(int arg_instanceId, int? arg_listenerInstanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.setDownloadListener', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.setDownloadListener', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_listenerInstanceId]) - as Map?; + await channel.send([arg_instanceId, arg_listenerInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -771,23 +835,18 @@ class WebViewHostApi { } } - Future setWebChromeClient( - int arg_instanceId, int arg_clientInstanceId) async { + Future setWebChromeClient(int arg_instanceId, int? arg_clientInstanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.setWebChromeClient', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.setWebChromeClient', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_clientInstanceId]) - as Map?; + await channel.send([arg_instanceId, arg_clientInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -800,19 +859,16 @@ class WebViewHostApi { Future setBackgroundColor(int arg_instanceId, int arg_color) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.setBackgroundColor', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_color]) as Map?; + 'dev.flutter.pigeon.WebViewHostApi.setBackgroundColor', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_color]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -824,37 +880,28 @@ class WebViewHostApi { } } -class _WebSettingsHostApiCodec extends StandardMessageCodec { - const _WebSettingsHostApiCodec(); -} class WebSettingsHostApi { /// Constructor for [WebSettingsHostApi]. 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. - WebSettingsHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - + WebSettingsHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _WebSettingsHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future create(int arg_instanceId, int arg_webViewInstanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.create', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.create', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_webViewInstanceId]) - as Map?; + await channel.send([arg_instanceId, arg_webViewInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -867,19 +914,16 @@ class WebSettingsHostApi { Future dispose(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.dispose', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.dispose', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -892,19 +936,16 @@ class WebSettingsHostApi { Future setDomStorageEnabled(int arg_instanceId, bool arg_flag) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_flag]) as Map?; + 'dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_flag]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -915,23 +956,18 @@ class WebSettingsHostApi { } } - Future setJavaScriptCanOpenWindowsAutomatically( - int arg_instanceId, bool arg_flag) async { + Future setJavaScriptCanOpenWindowsAutomatically(int arg_instanceId, bool arg_flag) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically', - codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_flag]) as Map?; + 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_flag]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -942,23 +978,18 @@ class WebSettingsHostApi { } } - Future setSupportMultipleWindows( - int arg_instanceId, bool arg_support) async { + Future setSupportMultipleWindows(int arg_instanceId, bool arg_support) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows', - codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_support]) as Map?; + 'dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_support]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -971,19 +1002,16 @@ class WebSettingsHostApi { Future setJavaScriptEnabled(int arg_instanceId, bool arg_flag) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_flag]) as Map?; + 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_flag]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -994,23 +1022,18 @@ class WebSettingsHostApi { } } - Future setUserAgentString( - int arg_instanceId, String arg_userAgentString) async { + Future setUserAgentString(int arg_instanceId, String? arg_userAgentString) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_userAgentString]) - as Map?; + await channel.send([arg_instanceId, arg_userAgentString]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -1021,23 +1044,18 @@ class WebSettingsHostApi { } } - Future setMediaPlaybackRequiresUserGesture( - int arg_instanceId, bool arg_require) async { + Future setMediaPlaybackRequiresUserGesture(int arg_instanceId, bool arg_require) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture', - codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_require]) as Map?; + 'dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_require]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -1050,19 +1068,16 @@ class WebSettingsHostApi { Future setSupportZoom(int arg_instanceId, bool arg_support) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_support]) as Map?; + 'dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_support]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -1073,22 +1088,18 @@ class WebSettingsHostApi { } } - Future setLoadWithOverviewMode( - int arg_instanceId, bool arg_overview) async { + Future setLoadWithOverviewMode(int arg_instanceId, bool arg_overview) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_overview]) as Map?; + 'dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_overview]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -1101,19 +1112,16 @@ class WebSettingsHostApi { Future setUseWideViewPort(int arg_instanceId, bool arg_use) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_use]) as Map?; + 'dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_use]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -1124,22 +1132,18 @@ class WebSettingsHostApi { } } - Future setDisplayZoomControls( - int arg_instanceId, bool arg_enabled) async { + Future setDisplayZoomControls(int arg_instanceId, bool arg_enabled) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_enabled]) as Map?; + 'dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_enabled]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -1150,22 +1154,18 @@ class WebSettingsHostApi { } } - Future setBuiltInZoomControls( - int arg_instanceId, bool arg_enabled) async { + Future setBuiltInZoomControls(int arg_instanceId, bool arg_enabled) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_enabled]) as Map?; + 'dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_enabled]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -1178,19 +1178,16 @@ class WebSettingsHostApi { Future setAllowFileAccess(int arg_instanceId, bool arg_enabled) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_enabled]) as Map?; + 'dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_enabled]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -1201,22 +1198,18 @@ class WebSettingsHostApi { } } - Future setGeolocationEnabled( - int arg_instanceId, bool arg_enabled) async { + Future setGeolocationEnabled(int arg_instanceId, bool arg_enabled) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setGeolocationEnabled', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_enabled]) as Map?; + 'dev.flutter.pigeon.WebSettingsHostApi.setGeolocationEnabled', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_enabled]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -1228,37 +1221,28 @@ class WebSettingsHostApi { } } -class _JavaScriptChannelHostApiCodec extends StandardMessageCodec { - const _JavaScriptChannelHostApiCodec(); -} class JavaScriptChannelHostApi { /// Constructor for [JavaScriptChannelHostApi]. 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. - JavaScriptChannelHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - + JavaScriptChannelHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _JavaScriptChannelHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future create(int arg_instanceId, String arg_channelName) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaScriptChannelHostApi.create', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.JavaScriptChannelHostApi.create', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId, arg_channelName]) - as Map?; + await channel.send([arg_instanceId, arg_channelName]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -1270,32 +1254,23 @@ class JavaScriptChannelHostApi { } } -class _JavaScriptChannelFlutterApiCodec extends StandardMessageCodec { - const _JavaScriptChannelFlutterApiCodec(); -} - abstract class JavaScriptChannelFlutterApi { - static const MessageCodec codec = - _JavaScriptChannelFlutterApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void dispose(int instanceId); void postMessage(int instanceId, String message); - static void setup(JavaScriptChannelFlutterApi? api, - {BinaryMessenger? binaryMessenger}) { + static void setup(JavaScriptChannelFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose 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.JavaScriptChannelFlutterApi.dispose was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose was null, expected non-null int.'); api.dispose(arg_instanceId!); return; }); @@ -1303,21 +1278,17 @@ abstract class JavaScriptChannelFlutterApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage 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.JavaScriptChannelFlutterApi.postMessage was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage was null, expected non-null int.'); final String? arg_message = (args[1] as String?); - assert(arg_message != null, - 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage was null, expected non-null String.'); + assert(arg_message != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage was null, expected non-null String.'); api.postMessage(arg_instanceId!, arg_message!); return; }); @@ -1326,38 +1297,28 @@ abstract class JavaScriptChannelFlutterApi { } } -class _WebViewClientHostApiCodec extends StandardMessageCodec { - const _WebViewClientHostApiCodec(); -} class WebViewClientHostApi { /// Constructor for [WebViewClientHostApi]. 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. - WebViewClientHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - + WebViewClientHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _WebViewClientHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); - Future create( - int arg_instanceId, bool arg_shouldOverrideUrlLoading) async { + Future create(int arg_instanceId, bool arg_shouldOverrideUrlLoading) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientHostApi.create', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_shouldOverrideUrlLoading]) - as Map?; + 'dev.flutter.pigeon.WebViewClientHostApi.create', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_shouldOverrideUrlLoading]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -1369,65 +1330,59 @@ class WebViewClientHostApi { } } -class _WebViewClientFlutterApiCodec extends StandardMessageCodec { +class _WebViewClientFlutterApiCodec extends StandardMessageCodec{ const _WebViewClientFlutterApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { if (value is WebResourceErrorData) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is WebResourceRequestData) { + } else + if (value is WebResourceRequestData) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else { + } else +{ super.writeValue(buffer, value); } } - @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return WebResourceErrorData.decode(readValue(buffer)!); - - case 129: + + case 129: return WebResourceRequestData.decode(readValue(buffer)!); - - default: + + default: return super.readValueOfType(type, buffer); + } } } - abstract class WebViewClientFlutterApi { static const MessageCodec codec = _WebViewClientFlutterApiCodec(); void dispose(int instanceId); 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 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}) { + static void setup(WebViewClientFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientFlutterApi.dispose', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewClientFlutterApi.dispose', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.dispose was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.dispose 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.WebViewClientFlutterApi.dispose was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.dispose was null, expected non-null int.'); api.dispose(arg_instanceId!); return; }); @@ -1435,24 +1390,19 @@ abstract class WebViewClientFlutterApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted 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.WebViewClientFlutterApi.onPageStarted was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted was null, expected non-null int.'); final int? arg_webViewInstanceId = (args[1] as int?); - assert(arg_webViewInstanceId != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted was null, expected non-null int.'); + assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted was null, expected non-null int.'); final String? arg_url = (args[2] as String?); - assert(arg_url != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted was null, expected non-null String.'); + assert(arg_url != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted was null, expected non-null String.'); api.onPageStarted(arg_instanceId!, arg_webViewInstanceId!, arg_url!); return; }); @@ -1460,24 +1410,19 @@ abstract class WebViewClientFlutterApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished 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.WebViewClientFlutterApi.onPageFinished was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished was null, expected non-null int.'); final int? arg_webViewInstanceId = (args[1] as int?); - assert(arg_webViewInstanceId != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished was null, expected non-null int.'); + assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished was null, expected non-null int.'); final String? arg_url = (args[2] as String?); - assert(arg_url != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished was null, expected non-null String.'); + assert(arg_url != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onPageFinished was null, expected non-null String.'); api.onPageFinished(arg_instanceId!, arg_webViewInstanceId!, arg_url!); return; }); @@ -1485,115 +1430,85 @@ abstract class WebViewClientFlutterApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError', - codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError 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.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null int.'); final int? arg_webViewInstanceId = (args[1] as int?); - assert(arg_webViewInstanceId != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null int.'); - final WebResourceRequestData? arg_request = - (args[2] as WebResourceRequestData?); - assert(arg_request != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null WebResourceRequestData.'); - final WebResourceErrorData? arg_error = - (args[3] as WebResourceErrorData?); - assert(arg_error != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null WebResourceErrorData.'); - api.onReceivedRequestError(arg_instanceId!, arg_webViewInstanceId!, - arg_request!, arg_error!); + assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null int.'); + final WebResourceRequestData? arg_request = (args[2] as WebResourceRequestData?); + assert(arg_request != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null WebResourceRequestData.'); + final WebResourceErrorData? arg_error = (args[3] as WebResourceErrorData?); + assert(arg_error != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError was null, expected non-null WebResourceErrorData.'); + api.onReceivedRequestError(arg_instanceId!, arg_webViewInstanceId!, arg_request!, arg_error!); return; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError 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.WebViewClientFlutterApi.onReceivedError was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null int.'); final int? arg_webViewInstanceId = (args[1] as int?); - assert(arg_webViewInstanceId != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null int.'); + assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null int.'); final int? arg_errorCode = (args[2] as int?); - assert(arg_errorCode != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null int.'); + assert(arg_errorCode != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null int.'); final String? arg_description = (args[3] as String?); - assert(arg_description != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null String.'); + assert(arg_description != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null String.'); final String? arg_failingUrl = (args[4] as String?); - assert(arg_failingUrl != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null String.'); - api.onReceivedError(arg_instanceId!, arg_webViewInstanceId!, - arg_errorCode!, arg_description!, arg_failingUrl!); + assert(arg_failingUrl != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedError was null, expected non-null String.'); + api.onReceivedError(arg_instanceId!, arg_webViewInstanceId!, arg_errorCode!, arg_description!, arg_failingUrl!); return; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading 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.WebViewClientFlutterApi.requestLoading was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading was null, expected non-null int.'); final int? arg_webViewInstanceId = (args[1] as int?); - assert(arg_webViewInstanceId != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading was null, expected non-null int.'); - final WebResourceRequestData? arg_request = - (args[2] as WebResourceRequestData?); - assert(arg_request != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading was null, expected non-null WebResourceRequestData.'); - api.requestLoading( - arg_instanceId!, arg_webViewInstanceId!, arg_request!); + assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading was null, expected non-null int.'); + final WebResourceRequestData? arg_request = (args[2] as WebResourceRequestData?); + assert(arg_request != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.requestLoading was null, expected non-null WebResourceRequestData.'); + api.requestLoading(arg_instanceId!, arg_webViewInstanceId!, arg_request!); return; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading 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.WebViewClientFlutterApi.urlLoading was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading was null, expected non-null int.'); final int? arg_webViewInstanceId = (args[1] as int?); - assert(arg_webViewInstanceId != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading was null, expected non-null int.'); + assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading was null, expected non-null int.'); final String? arg_url = (args[2] as String?); - assert(arg_url != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading was null, expected non-null String.'); + assert(arg_url != null, 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.urlLoading was null, expected non-null String.'); api.urlLoading(arg_instanceId!, arg_webViewInstanceId!, arg_url!); return; }); @@ -1602,36 +1517,28 @@ abstract class WebViewClientFlutterApi { } } -class _DownloadListenerHostApiCodec extends StandardMessageCodec { - const _DownloadListenerHostApiCodec(); -} class DownloadListenerHostApi { /// Constructor for [DownloadListenerHostApi]. 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. - DownloadListenerHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - + DownloadListenerHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _DownloadListenerHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future create(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DownloadListenerHostApi.create', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.DownloadListenerHostApi.create', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; + await channel.send([arg_instanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -1643,32 +1550,23 @@ class DownloadListenerHostApi { } } -class _DownloadListenerFlutterApiCodec extends StandardMessageCodec { - const _DownloadListenerFlutterApiCodec(); -} - abstract class DownloadListenerFlutterApi { - static const MessageCodec codec = _DownloadListenerFlutterApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void dispose(int instanceId); - void onDownloadStart(int instanceId, String url, String userAgent, - String contentDisposition, String mimetype, int contentLength); - static void setup(DownloadListenerFlutterApi? api, - {BinaryMessenger? binaryMessenger}) { + void onDownloadStart(int instanceId, String url, String userAgent, String contentDisposition, String mimetype, int contentLength); + static void setup(DownloadListenerFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DownloadListenerFlutterApi.dispose', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.DownloadListenerFlutterApi.dispose', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.dispose was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.dispose 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.DownloadListenerFlutterApi.dispose was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.dispose was null, expected non-null int.'); api.dispose(arg_instanceId!); return; }); @@ -1676,36 +1574,26 @@ abstract class DownloadListenerFlutterApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart', - codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart 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.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null int.'); final String? arg_url = (args[1] as String?); - assert(arg_url != null, - 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); + assert(arg_url != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); final String? arg_userAgent = (args[2] as String?); - assert(arg_userAgent != null, - 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); + assert(arg_userAgent != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); final String? arg_contentDisposition = (args[3] as String?); - assert(arg_contentDisposition != null, - 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); + assert(arg_contentDisposition != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); final String? arg_mimetype = (args[4] as String?); - assert(arg_mimetype != null, - 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); + assert(arg_mimetype != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null String.'); final int? arg_contentLength = (args[5] as int?); - assert(arg_contentLength != null, - 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null int.'); - api.onDownloadStart(arg_instanceId!, arg_url!, arg_userAgent!, - arg_contentDisposition!, arg_mimetype!, arg_contentLength!); + assert(arg_contentLength != null, 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart was null, expected non-null int.'); + api.onDownloadStart(arg_instanceId!, arg_url!, arg_userAgent!, arg_contentDisposition!, arg_mimetype!, arg_contentLength!); return; }); } @@ -1713,38 +1601,28 @@ abstract class DownloadListenerFlutterApi { } } -class _WebChromeClientHostApiCodec extends StandardMessageCodec { - const _WebChromeClientHostApiCodec(); -} class WebChromeClientHostApi { /// Constructor for [WebChromeClientHostApi]. 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. - WebChromeClientHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - + WebChromeClientHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _WebChromeClientHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); - Future create( - int arg_instanceId, int arg_webViewClientInstanceId) async { + Future create(int arg_instanceId, int arg_webViewClientInstanceId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebChromeClientHostApi.create', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_webViewClientInstanceId]) - as Map?; + 'dev.flutter.pigeon.WebChromeClientHostApi.create', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_webViewClientInstanceId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + final Map error = (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -1756,41 +1634,38 @@ class WebChromeClientHostApi { } } -class _FlutterAssetManagerHostApiCodec extends StandardMessageCodec { - const _FlutterAssetManagerHostApiCodec(); -} class FlutterAssetManagerHostApi { /// Constructor for [FlutterAssetManagerHostApi]. 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. - FlutterAssetManagerHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - + FlutterAssetManagerHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _FlutterAssetManagerHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future> list(String arg_path) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_path]) as Map?; + await channel.send([arg_path]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + 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(); } @@ -1798,56 +1673,49 @@ class FlutterAssetManagerHostApi { Future getAssetFilePathByName(String arg_name) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName', - codec, - binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_name]) as Map?; + await channel.send([arg_name]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + 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 String?)!; } } } -class _WebChromeClientFlutterApiCodec extends StandardMessageCodec { - const _WebChromeClientFlutterApiCodec(); -} - abstract class WebChromeClientFlutterApi { - static const MessageCodec codec = _WebChromeClientFlutterApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void dispose(int instanceId); void onProgressChanged(int instanceId, int webViewInstanceId, int progress); - static void setup(WebChromeClientFlutterApi? api, - {BinaryMessenger? binaryMessenger}) { + static void setup(WebChromeClientFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebChromeClientFlutterApi.dispose', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebChromeClientFlutterApi.dispose', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.dispose was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.dispose 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.dispose was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.dispose was null, expected non-null int.'); api.dispose(arg_instanceId!); return; }); @@ -1855,30 +1723,78 @@ abstract class WebChromeClientFlutterApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged', - codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged 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.onProgressChanged was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); final int? arg_webViewInstanceId = (args[1] as int?); - assert(arg_webViewInstanceId != null, - 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); + assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); final int? arg_progress = (args[2] as int?); - assert(arg_progress != null, - 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); - api.onProgressChanged( - arg_instanceId!, arg_webViewInstanceId!, arg_progress!); + assert(arg_progress != null, 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); + api.onProgressChanged(arg_instanceId!, arg_webViewInstanceId!, arg_progress!); return; }); } } } } + + +class WebStorageHostApi { + /// Constructor for [WebStorageHostApi]. 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. + WebStorageHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future create(int arg_instanceId) async { + 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) { + 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 deleteAllData(int arg_instanceId) async { + 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) { + 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; + } + } +} 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 1bf560123d3b..dd3145b69ea3 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 @@ -2,9 +2,12 @@ // 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#106316) +// ignore: unnecessary_import import 'dart:typed_data'; +import 'dart:ui'; -import 'package:flutter/services.dart'; +import 'package:flutter/services.dart' show BinaryMessenger; import 'android_webview.dart'; import 'android_webview.pigeon.dart'; @@ -13,21 +16,20 @@ import 'instance_manager.dart'; /// Converts [WebResourceRequestData] to [WebResourceRequest] WebResourceRequest _toWebResourceRequest(WebResourceRequestData data) { return WebResourceRequest( - url: data.url!, - isForMainFrame: data.isForMainFrame!, + url: data.url, + isForMainFrame: data.isForMainFrame, isRedirect: data.isRedirect, - hasGesture: data.hasGesture!, - method: data.method!, - requestHeaders: - data.requestHeaders?.cast() ?? {}, + hasGesture: data.hasGesture, + method: data.method, + requestHeaders: data.requestHeaders.cast(), ); } /// Converts [WebResourceErrorData] to [WebResourceError]. WebResourceError _toWebResourceError(WebResourceErrorData data) { return WebResourceError( - errorCode: data.errorCode!, - description: data.description!, + errorCode: data.errorCode, + description: data.description, ); } @@ -35,11 +37,14 @@ WebResourceError _toWebResourceError(WebResourceErrorData data) { class AndroidWebViewFlutterApis { /// Creates a [AndroidWebViewFlutterApis]. AndroidWebViewFlutterApis({ + JavaObjectFlutterApiImpl? javaObjectFlutterApi, DownloadListenerFlutterApiImpl? downloadListenerFlutterApi, WebViewClientFlutterApiImpl? webViewClientFlutterApi, WebChromeClientFlutterApiImpl? webChromeClientFlutterApi, JavaScriptChannelFlutterApiImpl? javaScriptChannelFlutterApi, }) { + this.javaObjectFlutterApi = + javaObjectFlutterApi ?? JavaObjectFlutterApiImpl(); this.downloadListenerFlutterApi = downloadListenerFlutterApi ?? DownloadListenerFlutterApiImpl(); this.webViewClientFlutterApi = @@ -57,6 +62,9 @@ class AndroidWebViewFlutterApis { /// This should only be changed for testing purposes. static AndroidWebViewFlutterApis instance = AndroidWebViewFlutterApis(); + /// Handles callbacks methods for the native Java Object class. + late final JavaObjectFlutterApi javaObjectFlutterApi; + /// Flutter Api for [DownloadListener]. late final DownloadListenerFlutterApiImpl downloadListenerFlutterApi; @@ -72,6 +80,7 @@ class AndroidWebViewFlutterApis { /// Ensures all the Flutter APIs have been setup to receive calls from native code. void ensureSetUp() { if (!_haveBeenSetUp) { + JavaObjectFlutterApi.setup(javaObjectFlutterApi); DownloadListenerFlutterApi.setup(downloadListenerFlutterApi); WebViewClientFlutterApi.setup(webViewClientFlutterApi); WebChromeClientFlutterApi.setup(webChromeClientFlutterApi); @@ -81,45 +90,78 @@ class AndroidWebViewFlutterApis { } } +/// Handles methods calls to the native Java Object class. +class JavaObjectHostApiImpl extends JavaObjectHostApi { + /// Constructs a [JavaObjectHostApiImpl]. + JavaObjectHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + 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; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; +} + +/// Handles callbacks methods for the native Java Object class. +class JavaObjectFlutterApiImpl implements JavaObjectFlutterApi { + /// Constructs a [JavaObjectFlutterApiImpl]. + JavaObjectFlutterApiImpl({InstanceManager? instanceManager}) + : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void dispose(int identifier) { + instanceManager.remove(identifier); + } +} + /// Host api implementation for [WebView]. class WebViewHostApiImpl extends WebViewHostApi { /// Constructs a [WebViewHostApiImpl]. WebViewHostApiImpl({ BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - }) : super(binaryMessenger: binaryMessenger) { - this.instanceManager = instanceManager ?? InstanceManager.instance; - } + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); /// Maintains instances stored to communicate with java objects. - late final InstanceManager instanceManager; + final InstanceManager instanceManager; /// Helper method to convert instances ids to objects. - Future createFromInstance(WebView instance) async { - final int? instanceId = instanceManager.tryAddInstance(instance); - if (instanceId != null) { - return create(instanceId, instance.useHybridComposition); - } + Future createFromInstance(WebView instance) { + return create( + instanceManager.addDartCreatedInstance(instance), + instance.useHybridComposition, + ); } /// Helper method to convert instances ids to objects. Future disposeFromInstance(WebView instance) async { - final int? instanceId = instanceManager.getInstanceId(instance); + final int? instanceId = instanceManager.getIdentifier(instance); if (instanceId != null) { + instanceManager.remove(instanceId); await dispose(instanceId); } - instanceManager.removeInstance(instance); } /// Helper method to convert the instances ids to objects. Future loadDataFromInstance( WebView instance, String data, - String mimeType, - String encoding, + String? mimeType, + String? encoding, ) { return loadData( - instanceManager.getInstanceId(instance)!, + instanceManager.getIdentifier(instance)!, data, mimeType, encoding, @@ -129,14 +171,14 @@ class WebViewHostApiImpl extends WebViewHostApi { /// Helper method to convert instances ids to objects. Future loadDataWithBaseUrlFromInstance( WebView instance, - String baseUrl, + String? baseUrl, String data, - String mimeType, - String encoding, - String historyUrl, + String? mimeType, + String? encoding, + String? historyUrl, ) { return loadDataWithBaseUrl( - instanceManager.getInstanceId(instance)!, + instanceManager.getIdentifier(instance)!, baseUrl, data, mimeType, @@ -151,7 +193,7 @@ class WebViewHostApiImpl extends WebViewHostApi { String url, Map headers, ) { - return loadUrl(instanceManager.getInstanceId(instance)!, url, headers); + return loadUrl(instanceManager.getIdentifier(instance)!, url, headers); } /// Helper method to convert instances ids to objects. @@ -160,79 +202,88 @@ class WebViewHostApiImpl extends WebViewHostApi { String url, Uint8List data, ) { - return postUrl(instanceManager.getInstanceId(instance)!, url, data); + return postUrl(instanceManager.getIdentifier(instance)!, url, data); } /// Helper method to convert instances ids to objects. - Future getUrlFromInstance(WebView instance) { - return getUrl(instanceManager.getInstanceId(instance)!); + Future getUrlFromInstance(WebView instance) { + return getUrl(instanceManager.getIdentifier(instance)!); } /// Helper method to convert instances ids to objects. Future canGoBackFromInstance(WebView instance) { - return canGoBack(instanceManager.getInstanceId(instance)!); + return canGoBack(instanceManager.getIdentifier(instance)!); } /// Helper method to convert instances ids to objects. Future canGoForwardFromInstance(WebView instance) { - return canGoForward(instanceManager.getInstanceId(instance)!); + return canGoForward(instanceManager.getIdentifier(instance)!); } /// Helper method to convert instances ids to objects. Future goBackFromInstance(WebView instance) { - return goBack(instanceManager.getInstanceId(instance)!); + return goBack(instanceManager.getIdentifier(instance)!); } /// Helper method to convert instances ids to objects. Future goForwardFromInstance(WebView instance) { - return goForward(instanceManager.getInstanceId(instance)!); + return goForward(instanceManager.getIdentifier(instance)!); } /// Helper method to convert instances ids to objects. Future reloadFromInstance(WebView instance) { - return reload(instanceManager.getInstanceId(instance)!); + return reload(instanceManager.getIdentifier(instance)!); } /// Helper method to convert instances ids to objects. Future clearCacheFromInstance(WebView instance, bool includeDiskFiles) { return clearCache( - instanceManager.getInstanceId(instance)!, + instanceManager.getIdentifier(instance)!, includeDiskFiles, ); } /// Helper method to convert instances ids to objects. - Future evaluateJavascriptFromInstance( + Future evaluateJavascriptFromInstance( WebView instance, String javascriptString, ) { return evaluateJavascript( - instanceManager.getInstanceId(instance)!, javascriptString); + instanceManager.getIdentifier(instance)!, + javascriptString, + ); } /// Helper method to convert instances ids to objects. - Future getTitleFromInstance(WebView instance) { - return getTitle(instanceManager.getInstanceId(instance)!); + Future getTitleFromInstance(WebView instance) { + return getTitle(instanceManager.getIdentifier(instance)!); } /// Helper method to convert instances ids to objects. Future scrollToFromInstance(WebView instance, int x, int y) { - return scrollTo(instanceManager.getInstanceId(instance)!, x, y); + return scrollTo(instanceManager.getIdentifier(instance)!, x, y); } /// Helper method to convert instances ids to objects. Future scrollByFromInstance(WebView instance, int x, int y) { - return scrollBy(instanceManager.getInstanceId(instance)!, x, y); + return scrollBy(instanceManager.getIdentifier(instance)!, x, y); } /// Helper method to convert instances ids to objects. Future getScrollXFromInstance(WebView instance) { - return getScrollX(instanceManager.getInstanceId(instance)!); + return getScrollX(instanceManager.getIdentifier(instance)!); } /// Helper method to convert instances ids to objects. Future getScrollYFromInstance(WebView instance) { - return getScrollY(instanceManager.getInstanceId(instance)!); + return getScrollY(instanceManager.getIdentifier(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future getScrollPositionFromInstance(WebView instance) async { + final WebViewPoint position = + await getScrollPosition(instanceManager.getIdentifier(instance)!); + return Offset(position.x.toDouble(), position.y.toDouble()); } /// Helper method to convert instances ids to objects. @@ -241,8 +292,8 @@ class WebViewHostApiImpl extends WebViewHostApi { WebViewClient webViewClient, ) { return setWebViewClient( - instanceManager.getInstanceId(instance)!, - instanceManager.getInstanceId(webViewClient)!, + instanceManager.getIdentifier(instance)!, + instanceManager.getIdentifier(webViewClient)!, ); } @@ -252,8 +303,8 @@ class WebViewHostApiImpl extends WebViewHostApi { JavaScriptChannel javaScriptChannel, ) { return addJavaScriptChannel( - instanceManager.getInstanceId(instance)!, - instanceManager.getInstanceId(javaScriptChannel)!, + instanceManager.getIdentifier(instance)!, + instanceManager.getIdentifier(javaScriptChannel)!, ); } @@ -263,36 +314,36 @@ class WebViewHostApiImpl extends WebViewHostApi { JavaScriptChannel javaScriptChannel, ) { return removeJavaScriptChannel( - instanceManager.getInstanceId(instance)!, - instanceManager.getInstanceId(javaScriptChannel)!, + instanceManager.getIdentifier(instance)!, + instanceManager.getIdentifier(javaScriptChannel)!, ); } /// Helper method to convert instances ids to objects. Future setDownloadListenerFromInstance( WebView instance, - DownloadListener listener, + DownloadListener? listener, ) { return setDownloadListener( - instanceManager.getInstanceId(instance)!, - instanceManager.getInstanceId(listener)!, + instanceManager.getIdentifier(instance)!, + listener != null ? instanceManager.getIdentifier(listener) : null, ); } /// Helper method to convert instances ids to objects. Future setWebChromeClientFromInstance( WebView instance, - WebChromeClient client, + WebChromeClient? client, ) { return setWebChromeClient( - instanceManager.getInstanceId(instance)!, - instanceManager.getInstanceId(client)!, + instanceManager.getIdentifier(instance)!, + client != null ? instanceManager.getIdentifier(client) : null, ); } /// Helper method to convert instances ids to objects. Future setBackgroundColorFromInstance(WebView instance, int color) { - return setBackgroundColor(instanceManager.getInstanceId(instance)!, color); + return setBackgroundColor(instanceManager.getIdentifier(instance)!, color); } } @@ -302,28 +353,25 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { WebSettingsHostApiImpl({ BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - }) : super(binaryMessenger: binaryMessenger) { - this.instanceManager = instanceManager ?? InstanceManager.instance; - } + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); /// Maintains instances stored to communicate with java objects. - late final InstanceManager instanceManager; + final InstanceManager instanceManager; /// Helper method to convert instances ids to objects. - Future createFromInstance(WebSettings instance, WebView webView) async { - final int? instanceId = instanceManager.tryAddInstance(instance); - if (instanceId != null) { - return create( - instanceId, - instanceManager.getInstanceId(webView)!, - ); - } + Future createFromInstance(WebSettings instance, WebView webView) { + return create( + instanceManager.addDartCreatedInstance(instance), + instanceManager.getIdentifier(webView)!, + ); } /// Helper method to convert instances ids to objects. Future disposeFromInstance(WebSettings instance) async { - final int? instanceId = instanceManager.removeInstance(instance); + final int? instanceId = instanceManager.getIdentifier(instance); if (instanceId != null) { + instanceManager.remove(instanceId); return dispose(instanceId); } } @@ -333,7 +381,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { WebSettings instance, bool flag, ) { - return setDomStorageEnabled(instanceManager.getInstanceId(instance)!, flag); + return setDomStorageEnabled(instanceManager.getIdentifier(instance)!, flag); } /// Helper method to convert instances ids to objects. @@ -342,7 +390,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { bool flag, ) { return setJavaScriptCanOpenWindowsAutomatically( - instanceManager.getInstanceId(instance)!, + instanceManager.getIdentifier(instance)!, flag, ); } @@ -353,7 +401,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { bool support, ) { return setSupportMultipleWindows( - instanceManager.getInstanceId(instance)!, support); + instanceManager.getIdentifier(instance)!, support); } /// Helper method to convert instances ids to objects. @@ -362,7 +410,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { bool flag, ) { return setJavaScriptEnabled( - instanceManager.getInstanceId(instance)!, + instanceManager.getIdentifier(instance)!, flag, ); } @@ -373,7 +421,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { bool flag, ) { return setGeolocationEnabled( - instanceManager.getInstanceId(instance)!, + instanceManager.getIdentifier(instance)!, flag, ); } @@ -381,10 +429,10 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { /// Helper method to convert instances ids to objects. Future setUserAgentStringFromInstance( WebSettings instance, - String userAgentString, + String? userAgentString, ) { return setUserAgentString( - instanceManager.getInstanceId(instance)!, + instanceManager.getIdentifier(instance)!, userAgentString, ); } @@ -395,7 +443,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { bool require, ) { return setMediaPlaybackRequiresUserGesture( - instanceManager.getInstanceId(instance)!, + instanceManager.getIdentifier(instance)!, require, ); } @@ -405,7 +453,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { WebSettings instance, bool support, ) { - return setSupportZoom(instanceManager.getInstanceId(instance)!, support); + return setSupportZoom(instanceManager.getIdentifier(instance)!, support); } /// Helper method to convert instances ids to objects. @@ -414,7 +462,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { bool overview, ) { return setLoadWithOverviewMode( - instanceManager.getInstanceId(instance)!, + instanceManager.getIdentifier(instance)!, overview, ); } @@ -424,7 +472,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { WebSettings instance, bool use, ) { - return setUseWideViewPort(instanceManager.getInstanceId(instance)!, use); + return setUseWideViewPort(instanceManager.getIdentifier(instance)!, use); } /// Helper method to convert instances ids to objects. @@ -433,7 +481,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { bool enabled, ) { return setDisplayZoomControls( - instanceManager.getInstanceId(instance)!, + instanceManager.getIdentifier(instance)!, enabled, ); } @@ -444,7 +492,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { bool enabled, ) { return setBuiltInZoomControls( - instanceManager.getInstanceId(instance)!, + instanceManager.getIdentifier(instance)!, enabled, ); } @@ -455,7 +503,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { bool enabled, ) { return setAllowFileAccess( - instanceManager.getInstanceId(instance)!, + instanceManager.getIdentifier(instance)!, enabled, ); } @@ -467,18 +515,20 @@ class JavaScriptChannelHostApiImpl extends JavaScriptChannelHostApi { JavaScriptChannelHostApiImpl({ BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - }) : super(binaryMessenger: binaryMessenger) { - this.instanceManager = instanceManager ?? InstanceManager.instance; - } + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); /// Maintains instances stored to communicate with java objects. - late final InstanceManager instanceManager; + final InstanceManager instanceManager; /// Helper method to convert instances ids to objects. Future createFromInstance(JavaScriptChannel instance) async { - final int? instanceId = instanceManager.tryAddInstance(instance); - if (instanceId != null) { - return create(instanceId, instance.channelName); + if (instanceManager.getIdentifier(instance) == null) { + final int identifier = instanceManager.addDartCreatedInstance(instance); + await create( + identifier, + instance.channelName, + ); } } } @@ -486,22 +536,21 @@ class JavaScriptChannelHostApiImpl extends JavaScriptChannelHostApi { /// Flutter api implementation for [JavaScriptChannel]. class JavaScriptChannelFlutterApiImpl extends JavaScriptChannelFlutterApi { /// Constructs a [JavaScriptChannelFlutterApiImpl]. - JavaScriptChannelFlutterApiImpl({InstanceManager? instanceManager}) { - this.instanceManager = instanceManager ?? InstanceManager.instance; - } + JavaScriptChannelFlutterApiImpl({InstanceManager? instanceManager}) + : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. - late final InstanceManager instanceManager; + final InstanceManager instanceManager; @override void dispose(int instanceId) { - instanceManager.removeInstance(instanceId); + instanceManager.remove(instanceId); } @override void postMessage(int instanceId, String message) { - final JavaScriptChannel? instance = - instanceManager.getInstance(instanceId) as JavaScriptChannel?; + final JavaScriptChannel? instance = instanceManager + .getInstanceWithWeakReference(instanceId) as JavaScriptChannel?; assert( instance != null, 'InstanceManager does not contain an JavaScriptChannel with instanceId: $instanceId', @@ -516,18 +565,17 @@ class WebViewClientHostApiImpl extends WebViewClientHostApi { WebViewClientHostApiImpl({ BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - }) : super(binaryMessenger: binaryMessenger) { - this.instanceManager = instanceManager ?? InstanceManager.instance; - } + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); /// Maintains instances stored to communicate with java objects. - late final InstanceManager instanceManager; + final InstanceManager instanceManager; /// Helper method to convert instances ids to objects. Future createFromInstance(WebViewClient instance) async { - final int? instanceId = instanceManager.tryAddInstance(instance); - if (instanceId != null) { - return create(instanceId, instance.shouldOverrideUrlLoading); + if (instanceManager.getIdentifier(instance) == null) { + final int identifier = instanceManager.addDartCreatedInstance(instance); + return create(identifier, instance.shouldOverrideUrlLoading); } } } @@ -535,24 +583,23 @@ class WebViewClientHostApiImpl extends WebViewClientHostApi { /// Flutter api implementation for [WebViewClient]. class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { /// Constructs a [WebViewClientFlutterApiImpl]. - WebViewClientFlutterApiImpl({InstanceManager? instanceManager}) { - this.instanceManager = instanceManager ?? InstanceManager.instance; - } + WebViewClientFlutterApiImpl({InstanceManager? instanceManager}) + : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. - late final InstanceManager instanceManager; + final InstanceManager instanceManager; @override void dispose(int instanceId) { - instanceManager.removeInstance(instanceId); + instanceManager.remove(instanceId); } @override void onPageFinished(int instanceId, int webViewInstanceId, String url) { - final WebViewClient? instance = - instanceManager.getInstance(instanceId) as WebViewClient?; - final WebView? webViewInstance = - instanceManager.getInstance(webViewInstanceId) as WebView?; + final WebViewClient? instance = instanceManager + .getInstanceWithWeakReference(instanceId) as WebViewClient?; + final WebView? webViewInstance = instanceManager + .getInstanceWithWeakReference(webViewInstanceId) as WebView?; assert( instance != null, 'InstanceManager does not contain an WebViewClient with instanceId: $instanceId', @@ -566,10 +613,10 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { @override void onPageStarted(int instanceId, int webViewInstanceId, String url) { - final WebViewClient? instance = - instanceManager.getInstance(instanceId) as WebViewClient?; - final WebView? webViewInstance = - instanceManager.getInstance(webViewInstanceId) as WebView?; + final WebViewClient? instance = instanceManager + .getInstanceWithWeakReference(instanceId) as WebViewClient?; + final WebView? webViewInstance = instanceManager + .getInstanceWithWeakReference(webViewInstanceId) as WebView?; assert( instance != null, 'InstanceManager does not contain an WebViewClient with instanceId: $instanceId', @@ -589,10 +636,10 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { String description, String failingUrl, ) { - final WebViewClient? instance = - instanceManager.getInstance(instanceId) as WebViewClient?; - final WebView? webViewInstance = - instanceManager.getInstance(webViewInstanceId) as WebView?; + final WebViewClient? instance = instanceManager + .getInstanceWithWeakReference(instanceId) as WebViewClient?; + final WebView? webViewInstance = instanceManager + .getInstanceWithWeakReference(webViewInstanceId) as WebView?; assert( instance != null, 'InstanceManager does not contain an WebViewClient with instanceId: $instanceId', @@ -617,10 +664,10 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { WebResourceRequestData request, WebResourceErrorData error, ) { - final WebViewClient? instance = - instanceManager.getInstance(instanceId) as WebViewClient?; - final WebView? webViewInstance = - instanceManager.getInstance(webViewInstanceId) as WebView?; + final WebViewClient? instance = instanceManager + .getInstanceWithWeakReference(instanceId) as WebViewClient?; + final WebView? webViewInstance = instanceManager + .getInstanceWithWeakReference(webViewInstanceId) as WebView?; assert( instance != null, 'InstanceManager does not contain an WebViewClient with instanceId: $instanceId', @@ -642,10 +689,10 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { int webViewInstanceId, WebResourceRequestData request, ) { - final WebViewClient? instance = - instanceManager.getInstance(instanceId) as WebViewClient?; - final WebView? webViewInstance = - instanceManager.getInstance(webViewInstanceId) as WebView?; + final WebViewClient? instance = instanceManager + .getInstanceWithWeakReference(instanceId) as WebViewClient?; + final WebView? webViewInstance = instanceManager + .getInstanceWithWeakReference(webViewInstanceId) as WebView?; assert( instance != null, 'InstanceManager does not contain an WebViewClient with instanceId: $instanceId', @@ -663,10 +710,10 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { int webViewInstanceId, String url, ) { - final WebViewClient? instance = - instanceManager.getInstance(instanceId) as WebViewClient?; - final WebView? webViewInstance = - instanceManager.getInstance(webViewInstanceId) as WebView?; + final WebViewClient? instance = instanceManager + .getInstanceWithWeakReference(instanceId) as WebViewClient?; + final WebView? webViewInstance = instanceManager + .getInstanceWithWeakReference(webViewInstanceId) as WebView?; assert( instance != null, 'InstanceManager does not contain an WebViewClient with instanceId: $instanceId', @@ -685,18 +732,17 @@ class DownloadListenerHostApiImpl extends DownloadListenerHostApi { DownloadListenerHostApiImpl({ BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - }) : super(binaryMessenger: binaryMessenger) { - this.instanceManager = instanceManager ?? InstanceManager.instance; - } + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); /// Maintains instances stored to communicate with java objects. - late final InstanceManager instanceManager; + final InstanceManager instanceManager; /// Helper method to convert instances ids to objects. Future createFromInstance(DownloadListener instance) async { - final int? instanceId = instanceManager.tryAddInstance(instance); - if (instanceId != null) { - return create(instanceId); + if (instanceManager.getIdentifier(instance) == null) { + final int identifier = instanceManager.addDartCreatedInstance(instance); + return create(identifier); } } } @@ -704,16 +750,15 @@ class DownloadListenerHostApiImpl extends DownloadListenerHostApi { /// Flutter api implementation for [DownloadListener]. class DownloadListenerFlutterApiImpl extends DownloadListenerFlutterApi { /// Constructs a [DownloadListenerFlutterApiImpl]. - DownloadListenerFlutterApiImpl({InstanceManager? instanceManager}) { - this.instanceManager = instanceManager ?? InstanceManager.instance; - } + DownloadListenerFlutterApiImpl({InstanceManager? instanceManager}) + : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. - late final InstanceManager instanceManager; + final InstanceManager instanceManager; @override void dispose(int instanceId) { - instanceManager.removeInstance(instanceId); + instanceManager.remove(instanceId); } @override @@ -725,8 +770,8 @@ class DownloadListenerFlutterApiImpl extends DownloadListenerFlutterApi { String mimetype, int contentLength, ) { - final DownloadListener? instance = - instanceManager.getInstance(instanceId) as DownloadListener?; + final DownloadListener? instance = instanceManager + .getInstanceWithWeakReference(instanceId) as DownloadListener?; assert( instance != null, 'InstanceManager does not contain an DownloadListener with instanceId: $instanceId', @@ -747,21 +792,23 @@ class WebChromeClientHostApiImpl extends WebChromeClientHostApi { WebChromeClientHostApiImpl({ BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - }) : super(binaryMessenger: binaryMessenger) { - this.instanceManager = instanceManager ?? InstanceManager.instance; - } + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); /// Maintains instances stored to communicate with java objects. - late final InstanceManager instanceManager; + final InstanceManager instanceManager; /// Helper method to convert instances ids to objects. Future createFromInstance( WebChromeClient instance, WebViewClient webViewClient, ) async { - final int? instanceId = instanceManager.tryAddInstance(instance); - if (instanceId != null) { - return create(instanceId, instanceManager.getInstanceId(webViewClient)!); + if (instanceManager.getIdentifier(instance) == null) { + final int identifier = instanceManager.addDartCreatedInstance(instance); + return create( + identifier, + instanceManager.getIdentifier(webViewClient)!, + ); } } } @@ -769,24 +816,23 @@ class WebChromeClientHostApiImpl extends WebChromeClientHostApi { /// Flutter api implementation for [DownloadListener]. class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { /// Constructs a [DownloadListenerFlutterApiImpl]. - WebChromeClientFlutterApiImpl({InstanceManager? instanceManager}) { - this.instanceManager = instanceManager ?? InstanceManager.instance; - } + WebChromeClientFlutterApiImpl({InstanceManager? instanceManager}) + : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. - late final InstanceManager instanceManager; + final InstanceManager instanceManager; @override void dispose(int instanceId) { - instanceManager.removeInstance(instanceId); + instanceManager.remove(instanceId); } @override void onProgressChanged(int instanceId, int webViewInstanceId, int progress) { - final WebChromeClient? instance = - instanceManager.getInstance(instanceId) as WebChromeClient?; - final WebView? webViewInstance = - instanceManager.getInstance(webViewInstanceId) as WebView?; + final WebChromeClient? instance = instanceManager + .getInstanceWithWeakReference(instanceId) as WebChromeClient?; + final WebView? webViewInstance = instanceManager + .getInstanceWithWeakReference(webViewInstanceId) as WebView?; assert( instance != null, 'InstanceManager does not contain an WebChromeClient with instanceId: $instanceId', @@ -798,3 +844,29 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { instance!.onProgressChanged(webViewInstance!, progress); } } + +/// Host api implementation for [WebStorage]. +class WebStorageHostApiImpl extends WebStorageHostApi { + /// Constructs a [WebStorageHostApiImpl]. + WebStorageHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Maintains instances stored to communicate with java objects. + final InstanceManager instanceManager; + + /// Helper method to convert instances ids to objects. + Future createFromInstance(WebStorage instance) async { + if (instanceManager.getIdentifier(instance) == null) { + final int identifier = instanceManager.addDartCreatedInstance(instance); + return create(identifier); + } + } + + /// Helper method to convert instances ids to objects. + Future deleteAllDataFromInstance(WebStorage instance) { + return deleteAllData(instanceManager.getIdentifier(instance)!); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/instance_manager.dart b/packages/webview_flutter/webview_flutter_android/lib/src/instance_manager.dart index e6dd2b2c2c1d..5892823aabd3 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/instance_manager.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/instance_manager.dart @@ -2,51 +2,200 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// Maintains instances stored to communicate with java objects. +import 'package:flutter/foundation.dart'; + +/// An immutable object that can provide functional copies of itself. +/// +/// All implementers are expected to be immutable as defined by the annotation. +// TODO(bparrishMines): Uncomment annotation once +// https://github.com/flutter/plugins/pull/5831 lands or when making a breaking +// change for https://github.com/flutter/flutter/issues/107199. +// @immutable +mixin Copyable { + /// Instantiates and returns a functionally identical object to oneself. + /// + /// Outside of tests, this method should only ever be called by + /// [InstanceManager]. + /// + /// Subclasses should always override their parent's implementation of this + /// method. + @protected + Copyable copy(); +} + +/// Maintains instances used to communicate with the native objects they +/// represent. +/// +/// Added instances are stored as weak references and their copies are stored +/// as strong references to maintain access to their variables and callback +/// methods. Both are stored with the same identifier. +/// +/// When a weak referenced instance becomes inaccessible, +/// [onWeakReferenceRemoved] is called with its associated identifier. +/// +/// If an instance is retrieved and has the possibility to be used, +/// (e.g. calling [getInstanceWithWeakReference]) a copy of the strong reference +/// is added as a weak reference with the same identifier. This prevents a +/// scenario where the weak referenced instance was released and then later +/// returned by the host platform. class InstanceManager { - final Map _instanceIdsToInstances = {}; - final Map _instancesToInstanceIds = {}; + /// Constructs an [InstanceManager]. + InstanceManager({required void Function(int) onWeakReferenceRemoved}) { + this.onWeakReferenceRemoved = (int identifier) { + _weakInstances.remove(identifier); + onWeakReferenceRemoved(identifier); + }; + _finalizer = Finalizer(this.onWeakReferenceRemoved); + } + + // Identifiers are locked to a specific range to avoid collisions with objects + // created simultaneously by the host platform. + // Host uses identifiers >= 2^16 and Dart is expected to use values n where, + // 0 <= n < 2^16. + static const int _maxDartCreatedIdentifier = 65536; - int _nextInstanceId = 0; + // Expando is used because it doesn't prevent its keys from becoming + // inaccessible. This allows the manager to efficiently retrieve an identifier + // of an instance without holding a strong reference to that instance. + // + // It also doesn't use `==` to search for identifiers, which would lead to an + // infinite loop when comparing an object to its copy. (i.e. which was caused + // by calling instanceManager.getIdentifier() inside of `==` while this was a + // HashMap). + final Expando _identifiers = Expando(); + final Map> _weakInstances = + >{}; + final Map _strongInstances = {}; + late final Finalizer _finalizer; + int _nextIdentifier = 0; - /// Global instance of [InstanceManager]. - static final InstanceManager instance = InstanceManager(); + /// Called when a weak referenced instance is removed by [removeWeakReference] + /// or becomes inaccessible. + late final void Function(int) onWeakReferenceRemoved; - /// Attempt to add a new instance. + /// Adds a new instance that was instantiated by Dart. + /// + /// In other words, Dart wants to add a new instance that will represent + /// an object that will be instantiated on the host platform. + /// + /// Throws assertion error if the instance has already been added. /// - /// Returns new if [instance] has already been added. Otherwise, it is added - /// with a new instance id. - int? tryAddInstance(Object instance) { - if (_instancesToInstanceIds.containsKey(instance)) { + /// Returns the randomly generated id of the [instance] added. + int addDartCreatedInstance(Copyable instance) { + assert(getIdentifier(instance) == null); + + final int identifier = _nextUniqueIdentifier(); + _addInstanceWithIdentifier(instance, identifier); + return identifier; + } + + /// Removes the instance, if present, and call [onWeakReferenceRemoved] with + /// its identifier. + /// + /// Returns the identifier associated with the removed instance. Otherwise, + /// `null` if the instance was not found in this manager. + /// + /// This does not remove the the strong referenced instance associated with + /// [instance]. This can be done with [remove]. + int? removeWeakReference(Copyable instance) { + final int? identifier = getIdentifier(instance); + if (identifier == null) { return null; } - final int instanceId = _nextInstanceId++; - _instancesToInstanceIds[instance] = instanceId; - _instanceIdsToInstances[instanceId] = instance; - return instanceId; + _identifiers[instance] = null; + _finalizer.detach(instance); + onWeakReferenceRemoved(identifier); + + return identifier; } - /// Remove the instance from the manager. + /// Removes [identifier] and its associated strongly referenced instance, if + /// present, from the manager. /// - /// Returns null if the instance is removed. Otherwise, return the instanceId - /// of the removed instance. - int? removeInstance(Object instance) { - final int? instanceId = _instancesToInstanceIds[instance]; - if (instanceId != null) { - _instancesToInstanceIds.remove(instance); - _instanceIdsToInstances.remove(instanceId); + /// Returns the strong referenced instance associated with [identifier] before + /// it was removed. Returns `null` if [identifier] was not associated with + /// any strong reference. + /// + /// This does not remove the the weak referenced instance associtated with + /// [identifier]. This can be done with [removeWeakReference]. + T? remove(int identifier) { + return _strongInstances.remove(identifier) as T?; + } + + /// Retrieves the instance associated with identifier. + /// + /// The value returned is chosen from the following order: + /// + /// 1. A weakly referenced instance associated with identifier. + /// 2. If the only instance associated with identifier is a strongly + /// referenced instance, a copy of the instance is added as a weak reference + /// with the same identifier. Returning the newly created copy. + /// 3. If no instance is associated with identifier, returns null. + /// + /// This method also expects the host `InstanceManager` to have a strong + /// reference to the instance the identifier is associated with. + T? getInstanceWithWeakReference(int identifier) { + final Copyable? weakInstance = _weakInstances[identifier]?.target; + + if (weakInstance == null) { + final Copyable? strongInstance = _strongInstances[identifier]; + if (strongInstance != null) { + final Copyable copy = strongInstance.copy(); + _identifiers[copy] = identifier; + _weakInstances[identifier] = WeakReference(copy); + _finalizer.attach(copy, identifier, detach: copy); + return copy as T; + } + return strongInstance as T?; } - return instanceId; + + return weakInstance as T; + } + + /// Retrieves the identifier associated with instance. + int? getIdentifier(Copyable instance) { + return _identifiers[instance]; + } + + /// Adds a new instance that was instantiated by the host platform. + /// + /// In other words, the host platform wants to add a new instance that + /// represents an object on the host platform. Stored with [identifier]. + /// + /// Throws assertion error if the instance or its identifier has already been + /// added. + /// + /// Returns unique identifier of the [instance] added. + void addHostCreatedInstance(Copyable instance, int identifier) { + assert(!containsIdentifier(identifier)); + assert(getIdentifier(instance) == null); + assert(identifier >= 0); + _addInstanceWithIdentifier(instance, identifier); + } + + void _addInstanceWithIdentifier(Copyable instance, int identifier) { + _identifiers[instance] = identifier; + _weakInstances[identifier] = WeakReference(instance); + _finalizer.attach(instance, identifier, detach: instance); + + final Copyable copy = instance.copy(); + _identifiers[copy] = identifier; + _strongInstances[identifier] = copy; } - /// Retrieve the Object paired with instanceId. - Object? getInstance(int instanceId) { - return _instanceIdsToInstances[instanceId]; + /// Whether this manager contains the given [identifier]. + bool containsIdentifier(int identifier) { + return _weakInstances.containsKey(identifier) || + _strongInstances.containsKey(identifier); } - /// Retrieve the instanceId paired with instance. - int? getInstanceId(Object instance) { - return _instancesToInstanceIds[instance]; + int _nextUniqueIdentifier() { + late int identifier; + do { + identifier = _nextIdentifier; + _nextIdentifier = (_nextIdentifier + 1) % _maxDartCreatedIdentifier; + } while (containsIdentifier(identifier)); + return identifier; } } diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart index 3c61f0ce6239..afeccc56d7c6 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart @@ -11,7 +11,6 @@ import 'package:flutter/widgets.dart'; import 'package:webview_pro_platform_interface/webview_flutter_platform_interface.dart'; import 'src/android_webview.dart'; -import 'src/instance_manager.dart'; import 'webview_android_widget.dart'; /// Builds an Android webview. @@ -55,8 +54,8 @@ class AndroidWebView implements WebViewPlatform { gestureRecognizers: gestureRecognizers, layoutDirection: Directionality.maybeOf(context) ?? TextDirection.rtl, - creationParams: - InstanceManager.instance.getInstanceId(controller.webView), + creationParams: JavaObject.globalInstanceManager + .getIdentifier(controller.webView), creationParamsCodec: const StandardMessageCodec(), ), ); diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart index 93b78cd4e6d7..1b7459d625d2 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart @@ -2,10 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:webview_pro_android/src/android_webview.dart' - as android_webview; +import 'package:webview_pro_android/src/android_webview.dart' as android_webview; import 'package:webview_pro_platform_interface/webview_flutter_platform_interface.dart'; + /// Handles all cookie operations for the current platform. class WebViewAndroidCookieManager extends WebViewCookieManagerPlatform { @override diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart index b48bbaf3b030..e17b751320fa 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart @@ -15,6 +15,7 @@ import 'src/android_webview.dart' as android_webview; class WebViewAndroidWidget extends StatefulWidget { /// Constructs a [WebViewAndroidWidget]. const WebViewAndroidWidget({ + Key? key, required this.creationParams, required this.useHybridComposition, required this.callbacksHandler, @@ -23,7 +24,8 @@ class WebViewAndroidWidget extends StatefulWidget { @visibleForTesting this.webViewProxy = const WebViewProxy(), @visibleForTesting this.flutterAssetManager = const android_webview.FlutterAssetManager(), - }); + @visibleForTesting this.webStorage, + }) : super(key: key); /// Initial parameters used to setup the WebView. final CreationParams creationParams; @@ -59,6 +61,9 @@ class WebViewAndroidWidget extends StatefulWidget { final Widget Function(WebViewAndroidPlatformController controller) onBuildWidget; + /// Manages the JavaScript storage APIs. + final android_webview.WebStorage? webStorage; + @override State createState() => _WebViewAndroidWidgetState(); } @@ -76,6 +81,7 @@ class _WebViewAndroidWidgetState extends State { javascriptChannelRegistry: widget.javascriptChannelRegistry, webViewProxy: widget.webViewProxy, flutterAssetManager: widget.flutterAssetManager, + webStorage: widget.webStorage, ); } @@ -102,7 +108,9 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { @visibleForTesting this.webViewProxy = const WebViewProxy(), @visibleForTesting this.flutterAssetManager = const android_webview.FlutterAssetManager(), - }) : assert(creationParams.webSettings?.hasNavigationDelegate != null), + @visibleForTesting android_webview.WebStorage? webStorage, + }) : webStorage = webStorage ?? android_webview.WebStorage.instance, + assert(creationParams.webSettings?.hasNavigationDelegate != null), super(callbacksHandler) { webView = webViewProxy.createWebView( useHybridComposition: useHybridComposition, @@ -160,6 +168,9 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { late final WebViewAndroidWebChromeClient webChromeClient = WebViewAndroidWebChromeClient(); + /// Manages the JavaScript storage APIs. + final android_webview.WebStorage webStorage; + /// Receive various notifications and requests for [android_webview.WebView]. @visibleForTesting WebViewAndroidWebViewClient get webViewClient => _webViewClient; @@ -254,7 +265,10 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { Future reload() => webView.reload(); @override - Future clearCache() => webView.clearCache(true); + Future clearCache() { + webView.clearCache(true); + return webStorage.deleteAllData(); + } @override Future updateSettings(WebSettings setting) async { @@ -644,8 +658,8 @@ class WebViewAndroidWebViewClient extends android_webview.WebViewClient { if (returnValue is bool && returnValue) { loadUrl!(url, {}); - } else { - (returnValue as Future).then((bool shouldLoadUrl) { + } else if (returnValue is Future) { + returnValue.then((bool shouldLoadUrl) { if (shouldLoadUrl) { loadUrl!(url, {}); } @@ -669,8 +683,8 @@ class WebViewAndroidWebViewClient extends android_webview.WebViewClient { if (returnValue is bool && returnValue) { loadUrl!(request.url, {}); - } else { - (returnValue as Future).then((bool shouldLoadUrl) { + } else if (returnValue is Future) { + returnValue.then((bool shouldLoadUrl) { if (shouldLoadUrl) { loadUrl!(request.url, {}); } diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart index a3f05db3f391..33a843afaf1f 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart @@ -10,18 +10,23 @@ import 'package:flutter/widgets.dart'; import 'package:webview_pro_platform_interface/webview_flutter_platform_interface.dart'; import 'src/android_webview.dart'; -import 'src/instance_manager.dart'; import 'webview_android.dart'; import 'webview_android_widget.dart'; -/// Android [WebViewPlatform] that uses [AndroidViewSurface] to build the [WebView] widget. +/// Android [WebViewPlatform] that uses [AndroidViewSurface] to build the +/// [WebView] widget. /// /// To use this, set [WebView.platform] to an instance of this class. /// -/// This implementation uses hybrid composition to render the [WebView] on +/// This implementation uses [AndroidViewSurface] to render the [WebView] on /// Android. It solves multiple issues related to accessibility and interaction /// with the [WebView] at the cost of some performance on Android versions below -/// 10. See https://github.com/flutter/flutter/wiki/Hybrid-Composition for more +/// 10. +/// +/// To support transparent backgrounds on all Android devices, this +/// implementation uses hybrid composition when the opacity of +/// `CreationParams.backgroundColor` is less than 1.0. See +/// https://github.com/flutter/flutter/wiki/Hybrid-Composition for more /// information. class SurfaceAndroidWebView extends AndroidWebView { @override @@ -53,16 +58,23 @@ class SurfaceAndroidWebView extends AndroidWebView { ); }, onCreatePlatformView: (PlatformViewCreationParams params) { - return PlatformViewsService.initSurfaceAndroidView( + final Color? backgroundColor = creationParams.backgroundColor; + return _createViewController( + // On some Android devices, transparent backgrounds can cause + // rendering issues on the non hybrid composition + // AndroidViewSurface. This switches the WebView to Hybrid + // Composition when the background color is not 100% opaque. + hybridComposition: + backgroundColor != null && backgroundColor.opacity < 1.0, id: params.id, viewType: 'plugins.flutter.io/webview', // WebView content is not affected by the Android view's layout direction, // we explicitly set it here so that the widget doesn't require an ambient // directionality. - layoutDirection: TextDirection.rtl, - creationParams: - InstanceManager.instance.getInstanceId(controller.webView), - creationParamsCodec: const StandardMessageCodec(), + layoutDirection: + Directionality.maybeOf(context) ?? TextDirection.ltr, + webViewIdentifier: JavaObject.globalInstanceManager + .getIdentifier(controller.webView)!, ) ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) ..addOnPlatformViewCreatedListener((int id) { @@ -76,4 +88,29 @@ class SurfaceAndroidWebView extends AndroidWebView { }, ); } + + AndroidViewController _createViewController({ + required bool hybridComposition, + required int id, + required String viewType, + required TextDirection layoutDirection, + required int webViewIdentifier, + }) { + if (hybridComposition) { + return PlatformViewsService.initExpensiveAndroidView( + id: id, + viewType: viewType, + layoutDirection: layoutDirection, + creationParams: webViewIdentifier, + creationParamsCodec: const StandardMessageCodec(), + ); + } + return PlatformViewsService.initSurfaceAndroidView( + id: id, + viewType: viewType, + layoutDirection: layoutDirection, + creationParams: webViewIdentifier, + creationParamsCodec: const StandardMessageCodec(), + ); + } } 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 9a4e00c9ddb6..b27f3c7cb121 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -4,18 +4,76 @@ import 'package:pigeon/pigeon.dart'; +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/android_webview.pigeon.dart', + dartTestOut: 'test/test_android_webview.pigeon.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', + 'found in the LICENSE file.', + ]), + javaOut: + 'android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java', + javaOptions: JavaOptions( + package: 'io.flutter.plugins.webviewflutter', + className: 'GeneratedAndroidWebView', + copyrightHeader: [ + '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 WebResourceRequestData { - String? url; - bool? isForMainFrame; + WebResourceRequestData( + this.url, + this.isForMainFrame, + this.isRedirect, + this.hasGesture, + this.method, + this.requestHeaders, + ); + + String url; + bool isForMainFrame; bool? isRedirect; - bool? hasGesture; - String? method; - Map? requestHeaders; + bool hasGesture; + String method; + Map requestHeaders; } class WebResourceErrorData { - int? errorCode; - String? description; + WebResourceErrorData(this.errorCode, this.description); + + int errorCode; + String description; +} + +class WebViewPoint { + WebViewPoint(this.x, this.y); + + int x; + int y; +} + +/// Handles methods calls to the native Java Object class. +/// +/// Also handles calls to remove the reference to an instance with `dispose`. +/// +/// See https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html. +@HostApi(dartHostTestHandler: 'TestJavaObjectHostApi') +abstract class JavaObjectHostApi { + void dispose(int identifier); +} + +/// Handles callbacks methods for the native Java Object class. +/// +/// See https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html. +@FlutterApi() +abstract class JavaObjectFlutterApi { + void dispose(int identifier); } @HostApi() @@ -35,17 +93,17 @@ abstract class WebViewHostApi { void loadData( int instanceId, String data, - String mimeType, - String encoding, + String? mimeType, + String? encoding, ); void loadDataWithBaseUrl( int instanceId, - String baseUrl, + String? baseUrl, String data, - String mimeType, - String encoding, - String historyUrl, + String? mimeType, + String? encoding, + String? historyUrl, ); void loadUrl( @@ -60,7 +118,7 @@ abstract class WebViewHostApi { Uint8List data, ); - String getUrl(int instanceId); + String? getUrl(int instanceId); bool canGoBack(int instanceId); @@ -75,12 +133,12 @@ abstract class WebViewHostApi { void clearCache(int instanceId, bool includeDiskFiles); @async - String evaluateJavascript( + String? evaluateJavascript( int instanceId, String javascriptString, ); - String getTitle(int instanceId); + String? getTitle(int instanceId); void scrollTo(int instanceId, int x, int y); @@ -90,6 +148,8 @@ abstract class WebViewHostApi { int getScrollY(int instanceId); + WebViewPoint getScrollPosition(int instanceId); + void setWebContentsDebuggingEnabled(bool enabled); void setWebViewClient(int instanceId, int webViewClientInstanceId); @@ -98,9 +158,9 @@ abstract class WebViewHostApi { void removeJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); - void setDownloadListener(int instanceId, int listenerInstanceId); + void setDownloadListener(int instanceId, int? listenerInstanceId); - void setWebChromeClient(int instanceId, int clientInstanceId); + void setWebChromeClient(int instanceId, int? clientInstanceId); void setBackgroundColor(int instanceId, int color); } @@ -119,7 +179,7 @@ abstract class WebSettingsHostApi { void setJavaScriptEnabled(int instanceId, bool flag); - void setUserAgentString(int instanceId, String userAgentString); + void setUserAgentString(int instanceId, String? userAgentString); void setMediaPlaybackRequiresUserGesture(int instanceId, bool require); @@ -224,3 +284,10 @@ abstract class WebChromeClientFlutterApi { void onProgressChanged(int instanceId, int webViewInstanceId, int progress); } + +@HostApi(dartHostTestHandler: 'TestWebStorageHostApi') +abstract class WebStorageHostApi { + void create(int instanceId); + + void deleteAllData(int instanceId); +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 97c932c390c5..6b75d546a1a8 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,11 +2,11 @@ name: webview_pro_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/wenzhiming/flutter-plugins/tree/dev/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/wenzhiming/flutter-plugins/issues -version: 2.8.3+3 +version: 2.10.4+1 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=3.0.0" flutter: plugin: @@ -28,6 +28,5 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - mockito: ^5.0.16 - pedantic: ^1.10.0 - pigeon: 1.0.9 + mockito: ^5.2.0 + pigeon: ^4.0.2 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 765729defa3f..b05e035965d3 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 @@ -10,17 +10,19 @@ import 'package:webview_pro_android/src/android_webview.pigeon.dart'; import 'package:webview_pro_android/src/android_webview_api_impls.dart'; import 'package:webview_pro_android/src/instance_manager.dart'; -import 'android_webview.pigeon.dart'; import 'android_webview_test.mocks.dart'; +import 'test_android_webview.pigeon.dart'; @GenerateMocks([ CookieManagerHostApi, DownloadListener, JavaScriptChannel, TestDownloadListenerHostApi, + TestJavaObjectHostApi, TestJavaScriptChannelHostApi, TestWebChromeClientHostApi, TestWebSettingsHostApi, + TestWebStorageHostApi, TestWebViewClientHostApi, TestWebViewHostApi, TestAssetManagerHostApi, @@ -32,6 +34,58 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Android WebView', () { + group('JavaObject', () { + late MockTestJavaObjectHostApi mockPlatformHostApi; + + setUp(() { + mockPlatformHostApi = MockTestJavaObjectHostApi(); + TestJavaObjectHostApi.setup(mockPlatformHostApi); + }); + + tearDown(() { + TestJavaObjectHostApi.setup(null); + }); + + test('JavaObject.dispose', () async { + int? callbackIdentifier; + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (int identifier) { + callbackIdentifier = identifier; + }, + ); + + final JavaObject object = JavaObject.detached( + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance(object, 0); + + JavaObject.dispose(object); + + expect(callbackIdentifier, 0); + }); + + test('JavaObjectFlutterApi.dispose', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final JavaObject object = JavaObject.detached( + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance(object, 0); + instanceManager.removeWeakReference(object); + + expect(instanceManager.containsIdentifier(0), isTrue); + + final JavaObjectFlutterApiImpl flutterApi = JavaObjectFlutterApiImpl( + instanceManager: instanceManager, + ); + flutterApi.dispose(0); + + expect(instanceManager.containsIdentifier(0), isFalse); + }); + }); + group('WebView', () { late MockTestWebViewHostApi mockPlatformHostApi; @@ -44,11 +98,11 @@ void main() { mockPlatformHostApi = MockTestWebViewHostApi(); TestWebViewHostApi.setup(mockPlatformHostApi); - instanceManager = InstanceManager(); + instanceManager = InstanceManager(onWeakReferenceRemoved: (_) {}); WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); webView = WebView(); - webViewInstanceId = instanceManager.getInstanceId(webView)!; + webViewInstanceId = instanceManager.getIdentifier(webView)!; }); test('create', () { @@ -80,12 +134,12 @@ void main() { }); test('loadData with null values', () { - webView.loadData(data: 'hello', mimeType: null, encoding: null); + webView.loadData(data: 'hello'); verify(mockPlatformHostApi.loadData( webViewInstanceId, 'hello', - '', - '', + null, + null, )); }); @@ -112,11 +166,11 @@ void main() { webView.loadDataWithBaseUrl(data: 'hello'); verify(mockPlatformHostApi.loadDataWithBaseUrl( webViewInstanceId, - '', + null, 'hello', - '', - '', - '', + null, + null, + null, )); }); @@ -198,6 +252,15 @@ void main() { expect(webView.getScrollY(), completion(56)); }); + test('getScrollPosition', () async { + when(mockPlatformHostApi.getScrollPosition(webViewInstanceId)) + .thenReturn(WebViewPoint(x: 2, y: 16)); + await expectLater( + webView.getScrollPosition(), + completion(const Offset(2.0, 16.0)), + ); + }); + test('setWebViewClient', () { TestWebViewClientHostApi.setup(MockTestWebViewClientHostApi()); WebViewClient.api = WebViewClientHostApiImpl( @@ -205,11 +268,12 @@ void main() { ); final WebViewClient mockWebViewClient = MockWebViewClient(); + when(mockWebViewClient.copy()).thenReturn(MockWebViewClient()); when(mockWebViewClient.shouldOverrideUrlLoading).thenReturn(false); webView.setWebViewClient(mockWebViewClient); final int webViewClientInstanceId = - instanceManager.getInstanceId(mockWebViewClient)!; + instanceManager.getIdentifier(mockWebViewClient)!; verify(mockPlatformHostApi.setWebViewClient( webViewInstanceId, webViewClientInstanceId, @@ -223,12 +287,13 @@ void main() { ); final JavaScriptChannel mockJavaScriptChannel = MockJavaScriptChannel(); + when(mockJavaScriptChannel.copy()).thenReturn(MockJavaScriptChannel()); when(mockJavaScriptChannel.channelName).thenReturn('aChannel'); webView.addJavaScriptChannel(mockJavaScriptChannel); final int javaScriptChannelInstanceId = - instanceManager.getInstanceId(mockJavaScriptChannel)!; + instanceManager.getIdentifier(mockJavaScriptChannel)!; verify(mockPlatformHostApi.addJavaScriptChannel( webViewInstanceId, javaScriptChannelInstanceId, @@ -242,6 +307,7 @@ void main() { ); final JavaScriptChannel mockJavaScriptChannel = MockJavaScriptChannel(); + when(mockJavaScriptChannel.copy()).thenReturn(MockJavaScriptChannel()); when(mockJavaScriptChannel.channelName).thenReturn('aChannel'); expect( @@ -253,7 +319,7 @@ void main() { webView.removeJavaScriptChannel(mockJavaScriptChannel); final int javaScriptChannelInstanceId = - instanceManager.getInstanceId(mockJavaScriptChannel)!; + instanceManager.getIdentifier(mockJavaScriptChannel)!; verify(mockPlatformHostApi.removeJavaScriptChannel( webViewInstanceId, javaScriptChannelInstanceId, @@ -267,10 +333,11 @@ void main() { ); final DownloadListener mockDownloadListener = MockDownloadListener(); + when(mockDownloadListener.copy()).thenReturn(MockDownloadListener()); webView.setDownloadListener(mockDownloadListener); final int downloadListenerInstanceId = - instanceManager.getInstanceId(mockDownloadListener)!; + instanceManager.getIdentifier(mockDownloadListener)!; verify(mockPlatformHostApi.setDownloadListener( webViewInstanceId, downloadListenerInstanceId, @@ -284,6 +351,7 @@ void main() { instanceManager: instanceManager, ); final WebViewClient mockWebViewClient = MockWebViewClient(); + when(mockWebViewClient.copy()).thenReturn(MockWebViewClient()); when(mockWebViewClient.shouldOverrideUrlLoading).thenReturn(false); webView.setWebViewClient(mockWebViewClient); @@ -293,10 +361,11 @@ void main() { ); final WebChromeClient mockWebChromeClient = MockWebChromeClient(); + when(mockWebChromeClient.copy()).thenReturn(MockWebChromeClient()); webView.setWebChromeClient(mockWebChromeClient); final int webChromeClientInstanceId = - instanceManager.getInstanceId(mockWebChromeClient)!; + instanceManager.getIdentifier(mockWebChromeClient)!; verify(mockPlatformHostApi.setWebChromeClient( webViewInstanceId, webChromeClientInstanceId, @@ -311,12 +380,16 @@ void main() { WebSettings.api = WebSettingsHostApiImpl(instanceManager: instanceManager); final int webSettingsInstanceId = - instanceManager.getInstanceId(webView.settings)!; + instanceManager.getIdentifier(webView.settings)!; webView.release(); verify(mockWebSettingsPlatformHostApi.dispose(webSettingsInstanceId)); verify(mockPlatformHostApi.dispose(webViewInstanceId)); }); + + test('copy', () { + expect(webView.copy(), isA()); + }); }); group('WebSettings', () { @@ -328,7 +401,7 @@ void main() { late int webSettingsInstanceId; setUp(() { - instanceManager = InstanceManager(); + instanceManager = InstanceManager(onWeakReferenceRemoved: (_) {}); TestWebViewHostApi.setup(MockTestWebViewHostApi()); WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); @@ -341,7 +414,7 @@ void main() { ); webSettings = WebSettings(WebView()); - webSettingsInstanceId = instanceManager.getInstanceId(webSettings)!; + webSettingsInstanceId = instanceManager.getIdentifier(webSettings)!; }); test('create', () { @@ -443,6 +516,10 @@ void main() { true, )); }); + + test('copy', () { + expect(webSettings.copy(), isA()); + }); }); group('JavaScriptChannel', () { @@ -454,14 +531,16 @@ void main() { late int mockJavaScriptChannelInstanceId; setUp(() { - instanceManager = InstanceManager(); + instanceManager = InstanceManager(onWeakReferenceRemoved: (_) {}); flutterApi = JavaScriptChannelFlutterApiImpl( instanceManager: instanceManager, ); mockJavaScriptChannel = MockJavaScriptChannel(); + when(mockJavaScriptChannel.copy()).thenReturn(MockJavaScriptChannel()); + mockJavaScriptChannelInstanceId = - instanceManager.tryAddInstance(mockJavaScriptChannel)!; + instanceManager.addDartCreatedInstance(mockJavaScriptChannel); }); test('postMessage', () { @@ -471,6 +550,13 @@ void main() { ); verify(mockJavaScriptChannel.postMessage('Hello, World!')); }); + + test('copy', () { + expect( + JavaScriptChannel.detached('channel').copy(), + isA(), + ); + }); }); group('WebViewClient', () { @@ -485,17 +571,20 @@ void main() { late int mockWebViewInstanceId; setUp(() { - instanceManager = InstanceManager(); + instanceManager = InstanceManager(onWeakReferenceRemoved: (_) {}); flutterApi = WebViewClientFlutterApiImpl( instanceManager: instanceManager, ); mockWebViewClient = MockWebViewClient(); + when(mockWebViewClient.copy()).thenReturn(MockWebViewClient()); mockWebViewClientInstanceId = - instanceManager.tryAddInstance(mockWebViewClient)!; + instanceManager.addDartCreatedInstance(mockWebViewClient); mockWebView = MockWebView(); - mockWebViewInstanceId = instanceManager.tryAddInstance(mockWebView)!; + when(mockWebView.copy()).thenReturn(MockWebView()); + mockWebViewInstanceId = + instanceManager.addDartCreatedInstance(mockWebView); }); test('onPageStarted', () { @@ -526,14 +615,15 @@ void main() { flutterApi.onReceivedRequestError( mockWebViewClientInstanceId, mockWebViewInstanceId, - WebResourceRequestData() - ..url = 'https://www.google.com' - ..isForMainFrame = true - ..hasGesture = true - ..method = 'POST', - WebResourceErrorData() - ..errorCode = 34 - ..description = 'error description', + WebResourceRequestData( + url: 'https://www.google.com', + isForMainFrame: true, + hasGesture: true, + method: 'POST', + isRedirect: false, + requestHeaders: {}, + ), + WebResourceErrorData(errorCode: 34, description: 'error description'), ); verify(mockWebViewClient.onReceivedRequestError( @@ -564,11 +654,14 @@ void main() { flutterApi.requestLoading( mockWebViewClientInstanceId, mockWebViewInstanceId, - WebResourceRequestData() - ..url = 'https://www.google.com' - ..isForMainFrame = true - ..hasGesture = true - ..method = 'POST', + WebResourceRequestData( + url: 'https://www.google.com', + isForMainFrame: true, + hasGesture: true, + method: 'POST', + isRedirect: true, + requestHeaders: {}, + ), ); verify(mockWebViewClient.requestLoading( @@ -586,6 +679,10 @@ void main() { 'https://www.google.com', )); }); + + test('copy', () { + expect(WebViewClient.detached().copy(), isA()); + }); }); group('DownloadListener', () { @@ -597,14 +694,15 @@ void main() { late int mockDownloadListenerInstanceId; setUp(() { - instanceManager = InstanceManager(); + instanceManager = InstanceManager(onWeakReferenceRemoved: (_) {}); flutterApi = DownloadListenerFlutterApiImpl( instanceManager: instanceManager, ); mockDownloadListener = MockDownloadListener(); + when(mockDownloadListener.copy()).thenReturn(MockDownloadListener()); mockDownloadListenerInstanceId = - instanceManager.tryAddInstance(mockDownloadListener)!; + instanceManager.addDartCreatedInstance(mockDownloadListener); }); test('onPageStarted', () { @@ -624,6 +722,10 @@ void main() { 45, )); }); + + test('copy', () { + expect(DownloadListener.detached().copy(), isA()); + }); }); group('WebChromeClient', () { @@ -638,17 +740,21 @@ void main() { late int mockWebViewInstanceId; setUp(() { - instanceManager = InstanceManager(); + instanceManager = InstanceManager(onWeakReferenceRemoved: (_) {}); flutterApi = WebChromeClientFlutterApiImpl( instanceManager: instanceManager, ); mockWebChromeClient = MockWebChromeClient(); + when(mockWebChromeClient.copy()).thenReturn(MockWebChromeClient()); + mockWebChromeClientInstanceId = - instanceManager.tryAddInstance(mockWebChromeClient)!; + instanceManager.addDartCreatedInstance(mockWebChromeClient); mockWebView = MockWebView(); - mockWebViewInstanceId = instanceManager.tryAddInstance(mockWebView)!; + when(mockWebView.copy()).thenReturn(MockWebView()); + mockWebViewInstanceId = + instanceManager.addDartCreatedInstance(mockWebView); }); test('onPageStarted', () { @@ -659,6 +765,10 @@ void main() { ); verify(mockWebChromeClient.onProgressChanged(mockWebView, 76)); }); + + test('copy', () { + expect(WebChromeClient.detached().copy(), isA()); + }); }); }); @@ -677,4 +787,33 @@ void main() { verify(CookieManager.api.clearCookies()); }); }); + + group('WebStorage', () { + late MockTestWebStorageHostApi mockPlatformHostApi; + + late WebStorage webStorage; + late int webStorageInstanceId; + + setUp(() { + mockPlatformHostApi = MockTestWebStorageHostApi(); + TestWebStorageHostApi.setup(mockPlatformHostApi); + + webStorage = WebStorage(); + webStorageInstanceId = + WebStorage.api.instanceManager.getIdentifier(webStorage)!; + }); + + test('create', () { + verify(mockPlatformHostApi.create(webStorageInstanceId)); + }); + + test('deleteAllData', () { + webStorage.deleteAllData(); + verify(mockPlatformHostApi.deleteAllData(webStorageInstanceId)); + }); + + test('copy', () { + expect(WebStorage.detached().copy(), isA()); + }); + }); } 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 8e20bff92c18..38bfb5563171 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 @@ -1,17 +1,19 @@ -// Mocks generated by Mockito 5.0.16 from annotations -// in webview_flutter_android/test/android_webview_test.dart. +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_pro_android/test/android_webview_test.dart. // Do not manually edit this file. -import 'dart:async' as _i4; -import 'dart:typed_data' as _i6; -import 'dart:ui' as _i7; +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; +import 'dart:typed_data' as _i7; +import 'dart:ui' as _i3; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_pro_android/src/android_webview.dart' as _i2; -import 'package:webview_pro_android/src/android_webview.pigeon.dart' as _i3; +import 'package:webview_pro_android/src/android_webview.pigeon.dart' as _i4; -import 'android_webview.pigeon.dart' as _i5; +import 'test_android_webview.pigeon.dart' as _i6; +// ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references @@ -20,29 +22,114 @@ import 'android_webview.pigeon.dart' as _i5; // 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 _FakeWebSettings_0 extends _i1.Fake implements _i2.WebSettings {} +class _FakeDownloadListener_0 extends _i1.SmartFake + implements _i2.DownloadListener { + _FakeDownloadListener_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeJavaScriptChannel_1 extends _i1.SmartFake + implements _i2.JavaScriptChannel { + _FakeJavaScriptChannel_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebChromeClient_2 extends _i1.SmartFake + implements _i2.WebChromeClient { + _FakeWebChromeClient_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebSettings_3 extends _i1.SmartFake implements _i2.WebSettings { + _FakeWebSettings_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeOffset_4 extends _i1.SmartFake implements _i3.Offset { + _FakeOffset_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebView_5 extends _i1.SmartFake implements _i2.WebView { + _FakeWebView_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebViewClient_6 extends _i1.SmartFake implements _i2.WebViewClient { + _FakeWebViewClient_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} /// A class which mocks [CookieManagerHostApi]. /// /// See the documentation for Mockito's code generation for more information. class MockCookieManagerHostApi extends _i1.Mock - implements _i3.CookieManagerHostApi { + implements _i4.CookieManagerHostApi { MockCookieManagerHostApi() { _i1.throwOnMissingStub(this); } @override - _i4.Future clearCookies() => - (super.noSuchMethod(Invocation.method(#clearCookies, []), - returnValue: Future.value(false)) as _i4.Future); - @override - _i4.Future setCookie(String? arg_url, String? arg_value) => - (super.noSuchMethod(Invocation.method(#setCookie, [arg_url, arg_value]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - String toString() => super.toString(); + _i5.Future clearCookies() => (super.noSuchMethod( + Invocation.method( + #clearCookies, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future setCookie( + String? arg_url, + String? arg_value, + ) => + (super.noSuchMethod( + Invocation.method( + #setCookie, + [ + arg_url, + arg_value, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [DownloadListener]. @@ -54,14 +141,40 @@ class MockDownloadListener extends _i1.Mock implements _i2.DownloadListener { } @override - void onDownloadStart(String? url, String? userAgent, - String? contentDisposition, String? mimetype, int? contentLength) => + void onDownloadStart( + String? url, + String? userAgent, + String? contentDisposition, + String? mimetype, + int? contentLength, + ) => super.noSuchMethod( - Invocation.method(#onDownloadStart, - [url, userAgent, contentDisposition, mimetype, contentLength]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + Invocation.method( + #onDownloadStart, + [ + url, + userAgent, + contentDisposition, + mimetype, + contentLength, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i2.DownloadListener copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeDownloadListener_0( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.DownloadListener); } /// A class which mocks [JavaScriptChannel]. @@ -73,324 +186,775 @@ class MockJavaScriptChannel extends _i1.Mock implements _i2.JavaScriptChannel { } @override - String get channelName => - (super.noSuchMethod(Invocation.getter(#channelName), returnValue: '') - as String); - @override - void postMessage(String? message) => - super.noSuchMethod(Invocation.method(#postMessage, [message]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + String get channelName => (super.noSuchMethod( + Invocation.getter(#channelName), + returnValue: '', + ) as String); + @override + void postMessage(String? message) => super.noSuchMethod( + Invocation.method( + #postMessage, + [message], + ), + returnValueForMissingStub: null, + ); + @override + _i2.JavaScriptChannel copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeJavaScriptChannel_1( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.JavaScriptChannel); } /// A class which mocks [TestDownloadListenerHostApi]. /// /// See the documentation for Mockito's code generation for more information. class MockTestDownloadListenerHostApi extends _i1.Mock - implements _i5.TestDownloadListenerHostApi { + implements _i6.TestDownloadListenerHostApi { MockTestDownloadListenerHostApi() { _i1.throwOnMissingStub(this); } @override - void create(int? instanceId) => - super.noSuchMethod(Invocation.method(#create, [instanceId]), - returnValueForMissingStub: null); + void create(int? instanceId) => super.noSuchMethod( + Invocation.method( + #create, + [instanceId], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [TestJavaObjectHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestJavaObjectHostApi extends _i1.Mock + implements _i6.TestJavaObjectHostApi { + MockTestJavaObjectHostApi() { + _i1.throwOnMissingStub(this); + } + @override - String toString() => super.toString(); + void dispose(int? identifier) => super.noSuchMethod( + Invocation.method( + #dispose, + [identifier], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestJavaScriptChannelHostApi]. /// /// See the documentation for Mockito's code generation for more information. class MockTestJavaScriptChannelHostApi extends _i1.Mock - implements _i5.TestJavaScriptChannelHostApi { + implements _i6.TestJavaScriptChannelHostApi { MockTestJavaScriptChannelHostApi() { _i1.throwOnMissingStub(this); } @override - void create(int? instanceId, String? channelName) => - super.noSuchMethod(Invocation.method(#create, [instanceId, channelName]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + void create( + int? instanceId, + String? channelName, + ) => + super.noSuchMethod( + Invocation.method( + #create, + [ + instanceId, + channelName, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWebChromeClientHostApi]. /// /// See the documentation for Mockito's code generation for more information. class MockTestWebChromeClientHostApi extends _i1.Mock - implements _i5.TestWebChromeClientHostApi { + implements _i6.TestWebChromeClientHostApi { MockTestWebChromeClientHostApi() { _i1.throwOnMissingStub(this); } @override - void create(int? instanceId, int? webViewClientInstanceId) => + void create( + int? instanceId, + int? webViewClientInstanceId, + ) => super.noSuchMethod( - Invocation.method(#create, [instanceId, webViewClientInstanceId]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + Invocation.method( + #create, + [ + instanceId, + webViewClientInstanceId, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWebSettingsHostApi]. /// /// See the documentation for Mockito's code generation for more information. class MockTestWebSettingsHostApi extends _i1.Mock - implements _i5.TestWebSettingsHostApi { + implements _i6.TestWebSettingsHostApi { MockTestWebSettingsHostApi() { _i1.throwOnMissingStub(this); } @override - void create(int? instanceId, int? webViewInstanceId) => super.noSuchMethod( - Invocation.method(#create, [instanceId, webViewInstanceId]), - returnValueForMissingStub: null); - @override - void dispose(int? instanceId) => - super.noSuchMethod(Invocation.method(#dispose, [instanceId]), - returnValueForMissingStub: null); - @override - void setDomStorageEnabled(int? instanceId, bool? flag) => super.noSuchMethod( - Invocation.method(#setDomStorageEnabled, [instanceId, flag]), - returnValueForMissingStub: null); - @override - void setJavaScriptCanOpenWindowsAutomatically(int? instanceId, bool? flag) => + void create( + int? instanceId, + int? webViewInstanceId, + ) => super.noSuchMethod( - Invocation.method( - #setJavaScriptCanOpenWindowsAutomatically, [instanceId, flag]), - returnValueForMissingStub: null); - @override - void setSupportMultipleWindows(int? instanceId, bool? support) => + Invocation.method( + #create, + [ + instanceId, + webViewInstanceId, + ], + ), + returnValueForMissingStub: null, + ); + @override + void dispose(int? instanceId) => super.noSuchMethod( + Invocation.method( + #dispose, + [instanceId], + ), + returnValueForMissingStub: null, + ); + @override + void setDomStorageEnabled( + int? instanceId, + bool? flag, + ) => super.noSuchMethod( - Invocation.method(#setSupportMultipleWindows, [instanceId, support]), - returnValueForMissingStub: null); - @override - void setJavaScriptEnabled(int? instanceId, bool? flag) => super.noSuchMethod( - Invocation.method(#setJavaScriptEnabled, [instanceId, flag]), - returnValueForMissingStub: null); - @override - void setUserAgentString(int? instanceId, String? userAgentString) => + Invocation.method( + #setDomStorageEnabled, + [ + instanceId, + flag, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setJavaScriptCanOpenWindowsAutomatically( + int? instanceId, + bool? flag, + ) => super.noSuchMethod( - Invocation.method(#setUserAgentString, [instanceId, userAgentString]), - returnValueForMissingStub: null); - @override - void setMediaPlaybackRequiresUserGesture(int? instanceId, bool? require) => + Invocation.method( + #setJavaScriptCanOpenWindowsAutomatically, + [ + instanceId, + flag, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setSupportMultipleWindows( + int? instanceId, + bool? support, + ) => super.noSuchMethod( - Invocation.method( - #setMediaPlaybackRequiresUserGesture, [instanceId, require]), - returnValueForMissingStub: null); - @override - void setSupportZoom(int? instanceId, bool? support) => super.noSuchMethod( - Invocation.method(#setSupportZoom, [instanceId, support]), - returnValueForMissingStub: null); - @override - void setLoadWithOverviewMode(int? instanceId, bool? overview) => + Invocation.method( + #setSupportMultipleWindows, + [ + instanceId, + support, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setJavaScriptEnabled( + int? instanceId, + bool? flag, + ) => super.noSuchMethod( - Invocation.method(#setLoadWithOverviewMode, [instanceId, overview]), - returnValueForMissingStub: null); - @override - void setUseWideViewPort(int? instanceId, bool? use) => super.noSuchMethod( - Invocation.method(#setUseWideViewPort, [instanceId, use]), - returnValueForMissingStub: null); - @override - void setDisplayZoomControls(int? instanceId, bool? enabled) => + Invocation.method( + #setJavaScriptEnabled, + [ + instanceId, + flag, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setUserAgentString( + int? instanceId, + String? userAgentString, + ) => super.noSuchMethod( - Invocation.method(#setDisplayZoomControls, [instanceId, enabled]), - returnValueForMissingStub: null); - @override - void setBuiltInZoomControls(int? instanceId, bool? enabled) => + Invocation.method( + #setUserAgentString, + [ + instanceId, + userAgentString, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setMediaPlaybackRequiresUserGesture( + int? instanceId, + bool? require, + ) => super.noSuchMethod( - Invocation.method(#setBuiltInZoomControls, [instanceId, enabled]), - returnValueForMissingStub: null); - @override - void setAllowFileAccess(int? instanceId, bool? enabled) => super.noSuchMethod( - Invocation.method(#setAllowFileAccess, [instanceId, enabled]), - returnValueForMissingStub: null); + Invocation.method( + #setMediaPlaybackRequiresUserGesture, + [ + instanceId, + require, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setSupportZoom( + int? instanceId, + bool? support, + ) => + super.noSuchMethod( + Invocation.method( + #setSupportZoom, + [ + instanceId, + support, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setLoadWithOverviewMode( + int? instanceId, + bool? overview, + ) => + super.noSuchMethod( + Invocation.method( + #setLoadWithOverviewMode, + [ + instanceId, + overview, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setUseWideViewPort( + int? instanceId, + bool? use, + ) => + super.noSuchMethod( + Invocation.method( + #setUseWideViewPort, + [ + instanceId, + use, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setDisplayZoomControls( + int? instanceId, + bool? enabled, + ) => + super.noSuchMethod( + Invocation.method( + #setDisplayZoomControls, + [ + instanceId, + enabled, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setBuiltInZoomControls( + int? instanceId, + bool? enabled, + ) => + super.noSuchMethod( + Invocation.method( + #setBuiltInZoomControls, + [ + instanceId, + enabled, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setAllowFileAccess( + int? instanceId, + bool? enabled, + ) => + super.noSuchMethod( + Invocation.method( + #setAllowFileAccess, + [ + instanceId, + enabled, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setGeolocationEnabled( + int? instanceId, + bool? enabled, + ) => + super.noSuchMethod( + Invocation.method( + #setGeolocationEnabled, + [ + instanceId, + enabled, + ], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [TestWebStorageHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebStorageHostApi extends _i1.Mock + implements _i6.TestWebStorageHostApi { + MockTestWebStorageHostApi() { + _i1.throwOnMissingStub(this); + } + @override - String toString() => super.toString(); + void create(int? instanceId) => super.noSuchMethod( + Invocation.method( + #create, + [instanceId], + ), + returnValueForMissingStub: null, + ); + @override + void deleteAllData(int? instanceId) => super.noSuchMethod( + Invocation.method( + #deleteAllData, + [instanceId], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWebViewClientHostApi]. /// /// See the documentation for Mockito's code generation for more information. class MockTestWebViewClientHostApi extends _i1.Mock - implements _i5.TestWebViewClientHostApi { + implements _i6.TestWebViewClientHostApi { MockTestWebViewClientHostApi() { _i1.throwOnMissingStub(this); } @override - void create(int? instanceId, bool? shouldOverrideUrlLoading) => + void create( + int? instanceId, + bool? shouldOverrideUrlLoading, + ) => super.noSuchMethod( - Invocation.method(#create, [instanceId, shouldOverrideUrlLoading]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + Invocation.method( + #create, + [ + instanceId, + shouldOverrideUrlLoading, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWebViewHostApi]. /// /// See the documentation for Mockito's code generation for more information. class MockTestWebViewHostApi extends _i1.Mock - implements _i5.TestWebViewHostApi { + implements _i6.TestWebViewHostApi { MockTestWebViewHostApi() { _i1.throwOnMissingStub(this); } @override - void create(int? instanceId, bool? useHybridComposition) => + void create( + int? instanceId, + bool? useHybridComposition, + ) => super.noSuchMethod( - Invocation.method(#create, [instanceId, useHybridComposition]), - returnValueForMissingStub: null); - @override - void dispose(int? instanceId) => - super.noSuchMethod(Invocation.method(#dispose, [instanceId]), - returnValueForMissingStub: null); + Invocation.method( + #create, + [ + instanceId, + useHybridComposition, + ], + ), + returnValueForMissingStub: null, + ); + @override + void dispose(int? instanceId) => super.noSuchMethod( + Invocation.method( + #dispose, + [instanceId], + ), + returnValueForMissingStub: null, + ); @override void loadData( - int? instanceId, String? data, String? mimeType, String? encoding) => + int? instanceId, + String? data, + String? mimeType, + String? encoding, + ) => super.noSuchMethod( - Invocation.method(#loadData, [instanceId, data, mimeType, encoding]), - returnValueForMissingStub: null); - @override - void loadDataWithBaseUrl(int? instanceId, String? baseUrl, String? data, - String? mimeType, String? encoding, String? historyUrl) => + Invocation.method( + #loadData, + [ + instanceId, + data, + mimeType, + encoding, + ], + ), + returnValueForMissingStub: null, + ); + @override + void loadDataWithBaseUrl( + int? instanceId, + String? baseUrl, + String? data, + String? mimeType, + String? encoding, + String? historyUrl, + ) => super.noSuchMethod( - Invocation.method(#loadDataWithBaseUrl, - [instanceId, baseUrl, data, mimeType, encoding, historyUrl]), - returnValueForMissingStub: null); - @override - void loadUrl(int? instanceId, String? url, Map? headers) => + Invocation.method( + #loadDataWithBaseUrl, + [ + instanceId, + baseUrl, + data, + mimeType, + encoding, + historyUrl, + ], + ), + returnValueForMissingStub: null, + ); + @override + void loadUrl( + int? instanceId, + String? url, + Map? headers, + ) => super.noSuchMethod( - Invocation.method(#loadUrl, [instanceId, url, headers]), - returnValueForMissingStub: null); - @override - void postUrl(int? instanceId, String? url, _i6.Uint8List? data) => - super.noSuchMethod(Invocation.method(#postUrl, [instanceId, url, data]), - returnValueForMissingStub: null); - @override - String getUrl(int? instanceId) => - (super.noSuchMethod(Invocation.method(#getUrl, [instanceId]), - returnValue: '') as String); - @override - bool canGoBack(int? instanceId) => - (super.noSuchMethod(Invocation.method(#canGoBack, [instanceId]), - returnValue: false) as bool); - @override - bool canGoForward(int? instanceId) => - (super.noSuchMethod(Invocation.method(#canGoForward, [instanceId]), - returnValue: false) as bool); - @override - void goBack(int? instanceId) => - super.noSuchMethod(Invocation.method(#goBack, [instanceId]), - returnValueForMissingStub: null); - @override - void goForward(int? instanceId) => - super.noSuchMethod(Invocation.method(#goForward, [instanceId]), - returnValueForMissingStub: null); - @override - void reload(int? instanceId) => - super.noSuchMethod(Invocation.method(#reload, [instanceId]), - returnValueForMissingStub: null); - @override - void clearCache(int? instanceId, bool? includeDiskFiles) => + Invocation.method( + #loadUrl, + [ + instanceId, + url, + headers, + ], + ), + returnValueForMissingStub: null, + ); + @override + void postUrl( + int? instanceId, + String? url, + _i7.Uint8List? data, + ) => super.noSuchMethod( - Invocation.method(#clearCache, [instanceId, includeDiskFiles]), - returnValueForMissingStub: null); - @override - _i4.Future evaluateJavascript( - int? instanceId, String? javascriptString) => + Invocation.method( + #postUrl, + [ + instanceId, + url, + data, + ], + ), + returnValueForMissingStub: null, + ); + @override + String? getUrl(int? instanceId) => (super.noSuchMethod(Invocation.method( + #getUrl, + [instanceId], + )) as String?); + @override + bool canGoBack(int? instanceId) => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [instanceId], + ), + returnValue: false, + ) as bool); + @override + bool canGoForward(int? instanceId) => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [instanceId], + ), + returnValue: false, + ) as bool); + @override + void goBack(int? instanceId) => super.noSuchMethod( + Invocation.method( + #goBack, + [instanceId], + ), + returnValueForMissingStub: null, + ); + @override + void goForward(int? instanceId) => super.noSuchMethod( + Invocation.method( + #goForward, + [instanceId], + ), + returnValueForMissingStub: null, + ); + @override + void reload(int? instanceId) => super.noSuchMethod( + Invocation.method( + #reload, + [instanceId], + ), + returnValueForMissingStub: null, + ); + @override + void clearCache( + int? instanceId, + bool? includeDiskFiles, + ) => + super.noSuchMethod( + Invocation.method( + #clearCache, + [ + instanceId, + includeDiskFiles, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i5.Future evaluateJavascript( + int? instanceId, + String? javascriptString, + ) => (super.noSuchMethod( - Invocation.method( - #evaluateJavascript, [instanceId, javascriptString]), - returnValue: Future.value('')) as _i4.Future); - @override - String getTitle(int? instanceId) => - (super.noSuchMethod(Invocation.method(#getTitle, [instanceId]), - returnValue: '') as String); - @override - void scrollTo(int? instanceId, int? x, int? y) => - super.noSuchMethod(Invocation.method(#scrollTo, [instanceId, x, y]), - returnValueForMissingStub: null); - @override - void scrollBy(int? instanceId, int? x, int? y) => - super.noSuchMethod(Invocation.method(#scrollBy, [instanceId, x, y]), - returnValueForMissingStub: null); - @override - int getScrollX(int? instanceId) => - (super.noSuchMethod(Invocation.method(#getScrollX, [instanceId]), - returnValue: 0) as int); - @override - int getScrollY(int? instanceId) => - (super.noSuchMethod(Invocation.method(#getScrollY, [instanceId]), - returnValue: 0) as int); + Invocation.method( + #evaluateJavascript, + [ + instanceId, + javascriptString, + ], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + String? getTitle(int? instanceId) => (super.noSuchMethod(Invocation.method( + #getTitle, + [instanceId], + )) as String?); + @override + void scrollTo( + int? instanceId, + int? x, + int? y, + ) => + super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + instanceId, + x, + y, + ], + ), + returnValueForMissingStub: null, + ); + @override + void scrollBy( + int? instanceId, + int? x, + int? y, + ) => + super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + instanceId, + x, + y, + ], + ), + returnValueForMissingStub: null, + ); + @override + int getScrollX(int? instanceId) => (super.noSuchMethod( + Invocation.method( + #getScrollX, + [instanceId], + ), + returnValue: 0, + ) as int); + @override + int getScrollY(int? instanceId) => (super.noSuchMethod( + Invocation.method( + #getScrollY, + [instanceId], + ), + returnValue: 0, + ) as int); + @override + dynamic getScrollPosition(int? instanceId) => + super.noSuchMethod(Invocation.method( + #getScrollPosition, + [instanceId], + )); @override void setWebContentsDebuggingEnabled(bool? enabled) => super.noSuchMethod( - Invocation.method(#setWebContentsDebuggingEnabled, [enabled]), - returnValueForMissingStub: null); - @override - void setWebViewClient(int? instanceId, int? webViewClientInstanceId) => + Invocation.method( + #setWebContentsDebuggingEnabled, + [enabled], + ), + returnValueForMissingStub: null, + ); + @override + void setWebViewClient( + int? instanceId, + int? webViewClientInstanceId, + ) => super.noSuchMethod( - Invocation.method( - #setWebViewClient, [instanceId, webViewClientInstanceId]), - returnValueForMissingStub: null); + Invocation.method( + #setWebViewClient, + [ + instanceId, + webViewClientInstanceId, + ], + ), + returnValueForMissingStub: null, + ); @override void addJavaScriptChannel( - int? instanceId, int? javaScriptChannelInstanceId) => + int? instanceId, + int? javaScriptChannelInstanceId, + ) => super.noSuchMethod( - Invocation.method( - #addJavaScriptChannel, [instanceId, javaScriptChannelInstanceId]), - returnValueForMissingStub: null); + Invocation.method( + #addJavaScriptChannel, + [ + instanceId, + javaScriptChannelInstanceId, + ], + ), + returnValueForMissingStub: null, + ); @override void removeJavaScriptChannel( - int? instanceId, int? javaScriptChannelInstanceId) => + int? instanceId, + int? javaScriptChannelInstanceId, + ) => super.noSuchMethod( - Invocation.method(#removeJavaScriptChannel, - [instanceId, javaScriptChannelInstanceId]), - returnValueForMissingStub: null); - @override - void setDownloadListener(int? instanceId, int? listenerInstanceId) => + Invocation.method( + #removeJavaScriptChannel, + [ + instanceId, + javaScriptChannelInstanceId, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setDownloadListener( + int? instanceId, + int? listenerInstanceId, + ) => super.noSuchMethod( - Invocation.method( - #setDownloadListener, [instanceId, listenerInstanceId]), - returnValueForMissingStub: null); - @override - void setWebChromeClient(int? instanceId, int? clientInstanceId) => + Invocation.method( + #setDownloadListener, + [ + instanceId, + listenerInstanceId, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setWebChromeClient( + int? instanceId, + int? clientInstanceId, + ) => super.noSuchMethod( - Invocation.method( - #setWebChromeClient, [instanceId, clientInstanceId]), - returnValueForMissingStub: null); - @override - void setBackgroundColor(int? instanceId, int? color) => super.noSuchMethod( - Invocation.method(#setBackgroundColor, [instanceId, color]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + Invocation.method( + #setWebChromeClient, + [ + instanceId, + clientInstanceId, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setBackgroundColor( + int? instanceId, + int? color, + ) => + super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [ + instanceId, + color, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestAssetManagerHostApi]. /// /// See the documentation for Mockito's code generation for more information. class MockTestAssetManagerHostApi extends _i1.Mock - implements _i5.TestAssetManagerHostApi { + implements _i6.TestAssetManagerHostApi { MockTestAssetManagerHostApi() { _i1.throwOnMissingStub(this); } @override - List list(String? path) => - (super.noSuchMethod(Invocation.method(#list, [path]), - returnValue: []) as List); - @override - String getAssetFilePathByName(String? name) => - (super.noSuchMethod(Invocation.method(#getAssetFilePathByName, [name]), - returnValue: '') as String); - @override - String toString() => super.toString(); + List list(String? path) => (super.noSuchMethod( + Invocation.method( + #list, + [path], + ), + returnValue: [], + ) as List); + @override + String getAssetFilePathByName(String? name) => (super.noSuchMethod( + Invocation.method( + #getAssetFilePathByName, + [name], + ), + returnValue: '', + ) as String); } /// A class which mocks [WebChromeClient]. @@ -402,11 +966,34 @@ class MockWebChromeClient extends _i1.Mock implements _i2.WebChromeClient { } @override - void onProgressChanged(_i2.WebView? webView, int? progress) => super - .noSuchMethod(Invocation.method(#onProgressChanged, [webView, progress]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + void onProgressChanged( + _i2.WebView? webView, + int? progress, + ) => + super.noSuchMethod( + Invocation.method( + #onProgressChanged, + [ + webView, + progress, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i2.WebChromeClient copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebChromeClient_2( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebChromeClient); } /// A class which mocks [WebView]. @@ -418,147 +1005,315 @@ class MockWebView extends _i1.Mock implements _i2.WebView { } @override - bool get useHybridComposition => - (super.noSuchMethod(Invocation.getter(#useHybridComposition), - returnValue: false) as bool); - @override - _i2.WebSettings get settings => - (super.noSuchMethod(Invocation.getter(#settings), - returnValue: _FakeWebSettings_0()) as _i2.WebSettings); - @override - _i4.Future loadData( - {String? data, String? mimeType, String? encoding}) => + bool get useHybridComposition => (super.noSuchMethod( + Invocation.getter(#useHybridComposition), + returnValue: false, + ) as bool); + @override + _i2.WebSettings get settings => (super.noSuchMethod( + Invocation.getter(#settings), + returnValue: _FakeWebSettings_3( + this, + Invocation.getter(#settings), + ), + ) as _i2.WebSettings); + @override + _i5.Future loadData({ + required String? data, + String? mimeType, + String? encoding, + }) => (super.noSuchMethod( - Invocation.method(#loadData, [], - {#data: data, #mimeType: mimeType, #encoding: encoding}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future loadDataWithBaseUrl( - {String? baseUrl, - String? data, - String? mimeType, - String? encoding, - String? historyUrl}) => + Invocation.method( + #loadData, + [], + { + #data: data, + #mimeType: mimeType, + #encoding: encoding, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadDataWithBaseUrl({ + String? baseUrl, + required String? data, + String? mimeType, + String? encoding, + String? historyUrl, + }) => (super.noSuchMethod( - Invocation.method(#loadDataWithBaseUrl, [], { + Invocation.method( + #loadDataWithBaseUrl, + [], + { #baseUrl: baseUrl, #data: data, #mimeType: mimeType, #encoding: encoding, - #historyUrl: historyUrl - }), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future loadUrl(String? url, Map? headers) => - (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future postUrl(String? url, _i6.Uint8List? data) => - (super.noSuchMethod(Invocation.method(#postUrl, [url, data]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future getUrl() => - (super.noSuchMethod(Invocation.method(#getUrl, []), - returnValue: Future.value()) as _i4.Future); - @override - _i4.Future canGoBack() => - (super.noSuchMethod(Invocation.method(#canGoBack, []), - returnValue: Future.value(false)) as _i4.Future); - @override - _i4.Future canGoForward() => - (super.noSuchMethod(Invocation.method(#canGoForward, []), - returnValue: Future.value(false)) as _i4.Future); - @override - _i4.Future goBack() => - (super.noSuchMethod(Invocation.method(#goBack, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future goForward() => - (super.noSuchMethod(Invocation.method(#goForward, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future reload() => - (super.noSuchMethod(Invocation.method(#reload, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future clearCache(bool? includeDiskFiles) => - (super.noSuchMethod(Invocation.method(#clearCache, [includeDiskFiles]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future evaluateJavascript(String? javascriptString) => (super - .noSuchMethod(Invocation.method(#evaluateJavascript, [javascriptString]), - returnValue: Future.value()) as _i4.Future); - @override - _i4.Future getTitle() => - (super.noSuchMethod(Invocation.method(#getTitle, []), - returnValue: Future.value()) as _i4.Future); - @override - _i4.Future scrollTo(int? x, int? y) => - (super.noSuchMethod(Invocation.method(#scrollTo, [x, y]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future scrollBy(int? x, int? y) => - (super.noSuchMethod(Invocation.method(#scrollBy, [x, y]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future getScrollX() => - (super.noSuchMethod(Invocation.method(#getScrollX, []), - returnValue: Future.value(0)) as _i4.Future); - @override - _i4.Future getScrollY() => - (super.noSuchMethod(Invocation.method(#getScrollY, []), - returnValue: Future.value(0)) as _i4.Future); - @override - _i4.Future setWebViewClient(_i2.WebViewClient? webViewClient) => - (super.noSuchMethod(Invocation.method(#setWebViewClient, [webViewClient]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future addJavaScriptChannel( - _i2.JavaScriptChannel? javaScriptChannel) => + #historyUrl: historyUrl, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadUrl( + String? url, + Map? headers, + ) => + (super.noSuchMethod( + Invocation.method( + #loadUrl, + [ + url, + headers, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future postUrl( + String? url, + _i7.Uint8List? data, + ) => + (super.noSuchMethod( + Invocation.method( + #postUrl, + [ + url, + data, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getUrl() => (super.noSuchMethod( + Invocation.method( + #getUrl, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future clearCache(bool? includeDiskFiles) => (super.noSuchMethod( + Invocation.method( + #clearCache, + [includeDiskFiles], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future evaluateJavascript(String? javascriptString) => (super.noSuchMethod( - Invocation.method(#addJavaScriptChannel, [javaScriptChannel]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + Invocation.method( + #evaluateJavascript, + [javascriptString], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future scrollTo( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + x, + y, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future scrollBy( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + x, + y, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getScrollX() => (super.noSuchMethod( + Invocation.method( + #getScrollX, + [], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future getScrollY() => (super.noSuchMethod( + Invocation.method( + #getScrollY, + [], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future<_i3.Offset> getScrollPosition() => (super.noSuchMethod( + Invocation.method( + #getScrollPosition, + [], + ), + returnValue: _i5.Future<_i3.Offset>.value(_FakeOffset_4( + this, + Invocation.method( + #getScrollPosition, + [], + ), + )), + ) as _i5.Future<_i3.Offset>); @override - _i4.Future removeJavaScriptChannel( + _i5.Future setWebViewClient(_i2.WebViewClient? webViewClient) => + (super.noSuchMethod( + Invocation.method( + #setWebViewClient, + [webViewClient], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future addJavaScriptChannel( _i2.JavaScriptChannel? javaScriptChannel) => (super.noSuchMethod( - Invocation.method(#removeJavaScriptChannel, [javaScriptChannel]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setDownloadListener(_i2.DownloadListener? listener) => - (super.noSuchMethod(Invocation.method(#setDownloadListener, [listener]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setWebChromeClient(_i2.WebChromeClient? client) => - (super.noSuchMethod(Invocation.method(#setWebChromeClient, [client]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setBackgroundColor(_i7.Color? color) => - (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future release() => - (super.noSuchMethod(Invocation.method(#release, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - String toString() => super.toString(); + Invocation.method( + #addJavaScriptChannel, + [javaScriptChannel], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeJavaScriptChannel( + _i2.JavaScriptChannel? javaScriptChannel) => + (super.noSuchMethod( + Invocation.method( + #removeJavaScriptChannel, + [javaScriptChannel], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setDownloadListener(_i2.DownloadListener? listener) => + (super.noSuchMethod( + Invocation.method( + #setDownloadListener, + [listener], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setWebChromeClient(_i2.WebChromeClient? client) => + (super.noSuchMethod( + Invocation.method( + #setWebChromeClient, + [client], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setBackgroundColor(_i3.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future release() => (super.noSuchMethod( + Invocation.method( + #release, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i2.WebView copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebView_5( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebView); } /// A class which mocks [WebViewClient]. @@ -570,38 +1325,118 @@ class MockWebViewClient extends _i1.Mock implements _i2.WebViewClient { } @override - bool get shouldOverrideUrlLoading => - (super.noSuchMethod(Invocation.getter(#shouldOverrideUrlLoading), - returnValue: false) as bool); - @override - void onPageStarted(_i2.WebView? webView, String? url) => - super.noSuchMethod(Invocation.method(#onPageStarted, [webView, url]), - returnValueForMissingStub: null); + bool get shouldOverrideUrlLoading => (super.noSuchMethod( + Invocation.getter(#shouldOverrideUrlLoading), + returnValue: false, + ) as bool); @override - void onPageFinished(_i2.WebView? webView, String? url) => - super.noSuchMethod(Invocation.method(#onPageFinished, [webView, url]), - returnValueForMissingStub: null); - @override - void onReceivedRequestError(_i2.WebView? webView, - _i2.WebResourceRequest? request, _i2.WebResourceError? error) => + void onPageStarted( + _i2.WebView? webView, + String? url, + ) => super.noSuchMethod( - Invocation.method(#onReceivedRequestError, [webView, request, error]), - returnValueForMissingStub: null); - @override - void onReceivedError(_i2.WebView? webView, int? errorCode, - String? description, String? failingUrl) => + Invocation.method( + #onPageStarted, + [ + webView, + url, + ], + ), + returnValueForMissingStub: null, + ); + @override + void onPageFinished( + _i2.WebView? webView, + String? url, + ) => super.noSuchMethod( + Invocation.method( + #onPageFinished, + [ + webView, + url, + ], + ), + returnValueForMissingStub: null, + ); + @override + void onReceivedRequestError( + _i2.WebView? webView, + _i2.WebResourceRequest? request, + _i2.WebResourceError? error, + ) => + super.noSuchMethod( + Invocation.method( + #onReceivedRequestError, + [ + webView, + request, + error, + ], + ), + returnValueForMissingStub: null, + ); + @override + void onReceivedError( + _i2.WebView? webView, + int? errorCode, + String? description, + String? failingUrl, + ) => + super.noSuchMethod( + Invocation.method( + #onReceivedError, + [ + webView, + errorCode, + description, + failingUrl, + ], + ), + returnValueForMissingStub: null, + ); + @override + void requestLoading( + _i2.WebView? webView, + _i2.WebResourceRequest? request, + ) => + super.noSuchMethod( + Invocation.method( + #requestLoading, + [ + webView, + request, + ], + ), + returnValueForMissingStub: null, + ); + @override + void urlLoading( + _i2.WebView? webView, + String? url, + ) => + super.noSuchMethod( + Invocation.method( + #urlLoading, + [ + webView, + url, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i2.WebViewClient copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebViewClient_6( + this, Invocation.method( - #onReceivedError, [webView, errorCode, description, failingUrl]), - returnValueForMissingStub: null); - @override - void requestLoading(_i2.WebView? webView, _i2.WebResourceRequest? request) => - super.noSuchMethod(Invocation.method(#requestLoading, [webView, request]), - returnValueForMissingStub: null); - @override - void urlLoading(_i2.WebView? webView, String? url) => - super.noSuchMethod(Invocation.method(#urlLoading, [webView, url]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + #copy, + [], + ), + ), + ) as _i2.WebViewClient); } diff --git a/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.dart b/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.dart index 6db012dba4bc..626765c45d2b 100644 --- a/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.dart @@ -7,29 +7,137 @@ import 'package:webview_pro_android/src/instance_manager.dart'; void main() { group('InstanceManager', () { - late InstanceManager testInstanceManager; + test('addHostCreatedInstance', () { + final CopyableObject object = CopyableObject(); - setUp(() { - testInstanceManager = InstanceManager(); + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance(object, 0); + + expect(instanceManager.getIdentifier(object), 0); + expect( + instanceManager.getInstanceWithWeakReference(0), + object, + ); + }); + + test('addHostCreatedInstance prevents already used objects and ids', () { + final CopyableObject object = CopyableObject(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance(object, 0); + + expect( + () => instanceManager.addHostCreatedInstance(object, 0), + throwsAssertionError, + ); + + expect( + () => instanceManager.addHostCreatedInstance(CopyableObject(), 0), + throwsAssertionError, + ); + }); + + test('addFlutterCreatedInstance', () { + final CopyableObject object = CopyableObject(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addDartCreatedInstance(object); + + final int? instanceId = instanceManager.getIdentifier(object); + expect(instanceId, isNotNull); + expect( + instanceManager.getInstanceWithWeakReference(instanceId!), + object, + ); + }); + + test('removeWeakReference', () { + final CopyableObject object = CopyableObject(); + + int? weakInstanceId; + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (int instanceId) { + weakInstanceId = instanceId; + }); + + instanceManager.addHostCreatedInstance(object, 0); + + expect(instanceManager.removeWeakReference(object), 0); + expect( + instanceManager.getInstanceWithWeakReference(0), + isA(), + ); + expect(weakInstanceId, 0); + }); + + test('removeWeakReference removes only weak reference', () { + final CopyableObject object = CopyableObject(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance(object, 0); + + expect(instanceManager.removeWeakReference(object), 0); + final CopyableObject copy = instanceManager.getInstanceWithWeakReference( + 0, + )!; + expect(identical(object, copy), isFalse); + }); + + test('removeStrongReference', () { + final CopyableObject object = CopyableObject(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance(object, 0); + instanceManager.removeWeakReference(object); + expect(instanceManager.remove(0), isA()); + expect(instanceManager.containsIdentifier(0), isFalse); }); - test('tryAddInstance', () { - final Object object = Object(); + test('removeStrongReference removes only strong reference', () { + final CopyableObject object = CopyableObject(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); - expect(testInstanceManager.tryAddInstance(object), 0); - expect(testInstanceManager.getInstanceId(object), 0); - expect(testInstanceManager.getInstance(0), object); - expect(testInstanceManager.tryAddInstance(object), null); + instanceManager.addHostCreatedInstance(object, 0); + expect(instanceManager.remove(0), isA()); + expect( + instanceManager.getInstanceWithWeakReference(0), + object, + ); }); - test('removeInstance', () { - final Object object = Object(); - testInstanceManager.tryAddInstance(object); + test('getInstance can add a new weak reference', () { + final CopyableObject object = CopyableObject(); - expect(testInstanceManager.removeInstance(object), 0); - expect(testInstanceManager.getInstanceId(object), null); - expect(testInstanceManager.getInstance(0), null); - expect(testInstanceManager.removeInstance(object), null); + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance(object, 0); + instanceManager.removeWeakReference(object); + + final CopyableObject newWeakCopy = + instanceManager.getInstanceWithWeakReference( + 0, + )!; + expect(identical(object, newWeakCopy), isFalse); }); }); } + +class CopyableObject with Copyable { + @override + Copyable copy() { + return CopyableObject(); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/test/surface_android_test.dart b/packages/webview_flutter/webview_flutter_android/test/surface_android_test.dart new file mode 100644 index 000000000000..63e752b419e6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/surface_android_test.dart @@ -0,0 +1,115 @@ +// 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/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_android/webview_surface_android.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('SurfaceAndroidWebView', () { + late List log; + + setUpAll(() { + SystemChannels.platform_views.setMockMethodCallHandler( + (MethodCall call) async { + log.add(call); + if (call.method == 'resize') { + return { + 'width': call.arguments['width'], + 'height': call.arguments['height'], + }; + } + }, + ); + }); + + tearDownAll(() { + SystemChannels.platform_views.setMockMethodCallHandler(null); + }); + + setUp(() { + log = []; + }); + + testWidgets( + 'uses hybrid composition when background color is not 100% opaque', + (WidgetTester tester) async { + await tester.pumpWidget(Builder(builder: (BuildContext context) { + return SurfaceAndroidWebView().build( + context: context, + creationParams: CreationParams( + backgroundColor: Colors.transparent, + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: false, + )), + javascriptChannelRegistry: JavascriptChannelRegistry(null), + webViewPlatformCallbacksHandler: + TestWebViewPlatformCallbacksHandler(), + ); + })); + await tester.pumpAndSettle(); + + final MethodCall createMethodCall = log[0]; + expect(createMethodCall.method, 'create'); + expect(createMethodCall.arguments, containsPair('hybrid', true)); + }); + + testWidgets('default text direction is ltr', (WidgetTester tester) async { + await tester.pumpWidget(Builder(builder: (BuildContext context) { + return SurfaceAndroidWebView().build( + context: context, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: false, + )), + javascriptChannelRegistry: JavascriptChannelRegistry(null), + webViewPlatformCallbacksHandler: + TestWebViewPlatformCallbacksHandler(), + ); + })); + await tester.pumpAndSettle(); + + final MethodCall createMethodCall = log[0]; + expect(createMethodCall.method, 'create'); + expect( + createMethodCall.arguments, + containsPair( + 'direction', + AndroidViewController.kAndroidLayoutDirectionLtr, + ), + ); + }); + }); +} + +class TestWebViewPlatformCallbacksHandler + implements WebViewPlatformCallbacksHandler { + @override + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) { + throw UnimplementedError(); + } + + @override + void onPageFinished(String url) {} + + @override + void onPageStarted(String url) {} + + @override + void onProgress(int progress) {} + + @override + void onWebResourceError(WebResourceError error) {} +} diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart similarity index 52% rename from packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart rename to packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart index 0cbb6d00e84d..ff43a116f592 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart @@ -1,68 +1,115 @@ -// Autogenerated from Pigeon (v1.0.9), do not edit directly. +// 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. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import -// @dart = 2.12 +// 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 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../lib/src/android_webview.pigeon.dart'; +import 'package:webview_pro_android/src/android_webview.pigeon.dart'; -class _TestWebViewHostApiCodec extends StandardMessageCodec { - const _TestWebViewHostApiCodec(); +/// Handles methods calls to the native Java Object class. +/// +/// Also handles calls to remove the reference to an instance with `dispose`. +/// +/// See https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html. +abstract class TestJavaObjectHostApi { + static const MessageCodec codec = StandardMessageCodec(); + + 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 _TestWebViewHostApiCodec extends StandardMessageCodec{ + const _TestWebViewHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WebViewPoint) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else +{ + super.writeValue(buffer, value); + } + } + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WebViewPoint.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + + } + } +} abstract class TestWebViewHostApi { static const MessageCodec codec = _TestWebViewHostApiCodec(); void create(int instanceId, bool useHybridComposition); void dispose(int instanceId); - 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 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); + 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); + 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 setDownloadListener(int instanceId, int? listenerInstanceId); + void setWebChromeClient(int instanceId, int? clientInstanceId); void setBackgroundColor(int instanceId, int color); - static void setup(TestWebViewHostApi? api, - {BinaryMessenger? binaryMessenger}) { + static void setup(TestWebViewHostApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.create', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.create', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.create was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.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.WebViewHostApi.create was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.create was null, expected non-null int.'); final bool? arg_useHybridComposition = (args[1] as bool?); - assert(arg_useHybridComposition != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.create was null, expected non-null bool.'); + assert(arg_useHybridComposition != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.create was null, expected non-null bool.'); api.create(arg_instanceId!, arg_useHybridComposition!); return {}; }); @@ -70,18 +117,15 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.dispose', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.dispose', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.dispose was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.dispose 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.WebViewHostApi.dispose was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.dispose was null, expected non-null int.'); api.dispose(arg_instanceId!); return {}; }); @@ -89,89 +133,61 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.loadData', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.loadData', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadData was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadData 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.WebViewHostApi.loadData was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadData was null, expected non-null int.'); final String? arg_data = (args[1] as String?); - assert(arg_data != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadData was null, expected non-null String.'); + assert(arg_data != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadData was null, expected non-null String.'); final String? arg_mimeType = (args[2] as String?); - assert(arg_mimeType != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadData was null, expected non-null String.'); final String? arg_encoding = (args[3] as String?); - assert(arg_encoding != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadData was null, expected non-null String.'); - api.loadData( - arg_instanceId!, arg_data!, arg_mimeType!, arg_encoding!); + api.loadData(arg_instanceId!, arg_data!, arg_mimeType, arg_encoding); return {}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl 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.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null int.'); final String? arg_baseUrl = (args[1] as String?); - assert(arg_baseUrl != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null String.'); final String? arg_data = (args[2] as String?); - assert(arg_data != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null String.'); + assert(arg_data != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null String.'); final String? arg_mimeType = (args[3] as String?); - assert(arg_mimeType != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null String.'); final String? arg_encoding = (args[4] as String?); - assert(arg_encoding != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null String.'); final String? arg_historyUrl = (args[5] as String?); - assert(arg_historyUrl != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl was null, expected non-null String.'); - api.loadDataWithBaseUrl(arg_instanceId!, arg_baseUrl!, arg_data!, - arg_mimeType!, arg_encoding!, arg_historyUrl!); + api.loadDataWithBaseUrl(arg_instanceId!, arg_baseUrl, arg_data!, arg_mimeType, arg_encoding, arg_historyUrl); return {}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.loadUrl', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.loadUrl', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl 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.WebViewHostApi.loadUrl was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null, expected non-null int.'); final String? arg_url = (args[1] as String?); - assert(arg_url != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null, expected non-null String.'); - final Map? arg_headers = - (args[2] as Map?)?.cast(); - assert(arg_headers != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null, expected non-null Map.'); + assert(arg_url != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null, expected non-null String.'); + final Map? arg_headers = (args[2] as Map?)?.cast(); + 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 {}; }); @@ -179,24 +195,19 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.postUrl', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.postUrl', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.postUrl was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.postUrl 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.WebViewHostApi.postUrl was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.postUrl was null, expected non-null int.'); final String? arg_url = (args[1] as String?); - assert(arg_url != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.postUrl was null, expected non-null String.'); + assert(arg_url != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.postUrl was null, expected non-null String.'); final Uint8List? arg_data = (args[2] as Uint8List?); - assert(arg_data != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.postUrl was null, expected non-null Uint8List.'); + 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 {}; }); @@ -204,37 +215,31 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.getUrl', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.getUrl', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.getUrl was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getUrl 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.WebViewHostApi.getUrl was null, expected non-null int.'); - final String output = api.getUrl(arg_instanceId!); + 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}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.canGoBack', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.canGoBack', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.canGoBack was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.canGoBack 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.WebViewHostApi.canGoBack was null, expected non-null int.'); + 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}; }); @@ -242,18 +247,15 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.canGoForward', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.canGoForward', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.canGoForward was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.canGoForward 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.WebViewHostApi.canGoForward was null, expected non-null int.'); + 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}; }); @@ -261,18 +263,15 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.goBack', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.goBack', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.goBack was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.goBack 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.WebViewHostApi.goBack was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.goBack was null, expected non-null int.'); api.goBack(arg_instanceId!); return {}; }); @@ -280,18 +279,15 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.goForward', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.goForward', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.goForward was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.goForward 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.WebViewHostApi.goForward was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.goForward was null, expected non-null int.'); api.goForward(arg_instanceId!); return {}; }); @@ -299,18 +295,15 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.reload', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.reload', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.reload was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.reload 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.WebViewHostApi.reload was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.reload was null, expected non-null int.'); api.reload(arg_instanceId!); return {}; }); @@ -318,21 +311,17 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.clearCache', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.clearCache', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.clearCache was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.clearCache 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.WebViewHostApi.clearCache was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.clearCache was null, expected non-null int.'); final bool? arg_includeDiskFiles = (args[1] as bool?); - assert(arg_includeDiskFiles != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.clearCache was null, expected non-null bool.'); + assert(arg_includeDiskFiles != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.clearCache was null, expected non-null bool.'); api.clearCache(arg_instanceId!, arg_includeDiskFiles!); return {}; }); @@ -340,66 +329,53 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.evaluateJavascript', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.evaluateJavascript', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.evaluateJavascript was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.evaluateJavascript 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.WebViewHostApi.evaluateJavascript was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.evaluateJavascript was null, expected non-null int.'); final String? arg_javascriptString = (args[1] as String?); - assert(arg_javascriptString != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.evaluateJavascript was null, expected non-null String.'); - final String output = await api.evaluateJavascript( - arg_instanceId!, arg_javascriptString!); + assert(arg_javascriptString != null, '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}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.getTitle', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.getTitle', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.getTitle was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getTitle 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.WebViewHostApi.getTitle was null, expected non-null int.'); - final String output = api.getTitle(arg_instanceId!); + 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}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.scrollTo', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.scrollTo', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo 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.WebViewHostApi.scrollTo was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo was null, expected non-null int.'); final int? arg_x = (args[1] as int?); - assert(arg_x != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo was null, expected non-null int.'); + assert(arg_x != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo was null, expected non-null int.'); final int? arg_y = (args[2] as int?); - assert(arg_y != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo was null, expected non-null int.'); + 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 {}; }); @@ -407,24 +383,19 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.scrollBy', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.scrollBy', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy 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.WebViewHostApi.scrollBy was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy was null, expected non-null int.'); final int? arg_x = (args[1] as int?); - assert(arg_x != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy was null, expected non-null int.'); + assert(arg_x != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy was null, expected non-null int.'); final int? arg_y = (args[2] as int?); - assert(arg_y != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy was null, expected non-null int.'); + 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 {}; }); @@ -432,18 +403,15 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.getScrollX', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.getScrollX', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollX was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollX 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.WebViewHostApi.getScrollX was null, expected non-null int.'); + 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}; }); @@ -451,18 +419,15 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.getScrollY', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.getScrollY', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollY was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollY 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.WebViewHostApi.getScrollY was null, expected non-null int.'); + 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}; }); @@ -470,19 +435,31 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled', - codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.getScrollPosition', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollPosition 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.WebViewHostApi.getScrollPosition was null, expected non-null int.'); + final WebViewPoint output = api.getScrollPosition(arg_instanceId!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled was null.'); final List args = (message as List?)!; final bool? arg_enabled = (args[0] as bool?); - assert(arg_enabled != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled was null, expected non-null bool.'); + assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled was null, expected non-null bool.'); api.setWebContentsDebuggingEnabled(arg_enabled!); return {}; }); @@ -490,21 +467,17 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.setWebViewClient', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.setWebViewClient', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebViewClient was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebViewClient 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.WebViewHostApi.setWebViewClient was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebViewClient was null, expected non-null int.'); final int? arg_webViewClientInstanceId = (args[1] as int?); - assert(arg_webViewClientInstanceId != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebViewClient was null, expected non-null int.'); + assert(arg_webViewClientInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebViewClient was null, expected non-null int.'); api.setWebViewClient(arg_instanceId!, arg_webViewClientInstanceId!); return {}; }); @@ -512,111 +485,87 @@ abstract class TestWebViewHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel 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.WebViewHostApi.addJavaScriptChannel was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel was null, expected non-null int.'); final int? arg_javaScriptChannelInstanceId = (args[1] as int?); - assert(arg_javaScriptChannelInstanceId != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel was null, expected non-null int.'); - api.addJavaScriptChannel( - arg_instanceId!, arg_javaScriptChannelInstanceId!); + assert(arg_javaScriptChannelInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel was null, expected non-null int.'); + api.addJavaScriptChannel(arg_instanceId!, arg_javaScriptChannelInstanceId!); return {}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel 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.WebViewHostApi.removeJavaScriptChannel was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel was null, expected non-null int.'); final int? arg_javaScriptChannelInstanceId = (args[1] as int?); - assert(arg_javaScriptChannelInstanceId != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel was null, expected non-null int.'); - api.removeJavaScriptChannel( - arg_instanceId!, arg_javaScriptChannelInstanceId!); + assert(arg_javaScriptChannelInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel was null, expected non-null int.'); + api.removeJavaScriptChannel(arg_instanceId!, arg_javaScriptChannelInstanceId!); return {}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.setDownloadListener', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.setDownloadListener', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.setDownloadListener was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setDownloadListener 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.WebViewHostApi.setDownloadListener was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setDownloadListener was null, expected non-null int.'); final int? arg_listenerInstanceId = (args[1] as int?); - assert(arg_listenerInstanceId != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.setDownloadListener was null, expected non-null int.'); - api.setDownloadListener(arg_instanceId!, arg_listenerInstanceId!); + api.setDownloadListener(arg_instanceId!, arg_listenerInstanceId); return {}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.setWebChromeClient', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.setWebChromeClient', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebChromeClient was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebChromeClient 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.WebViewHostApi.setWebChromeClient was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebChromeClient was null, expected non-null int.'); final int? arg_clientInstanceId = (args[1] as int?); - assert(arg_clientInstanceId != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebChromeClient was null, expected non-null int.'); - api.setWebChromeClient(arg_instanceId!, arg_clientInstanceId!); + api.setWebChromeClient(arg_instanceId!, arg_clientInstanceId); return {}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.setBackgroundColor', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewHostApi.setBackgroundColor', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.setBackgroundColor was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setBackgroundColor 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.WebViewHostApi.setBackgroundColor was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setBackgroundColor was null, expected non-null int.'); final int? arg_color = (args[1] as int?); - assert(arg_color != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.setBackgroundColor was null, expected non-null int.'); + assert(arg_color != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setBackgroundColor was null, expected non-null int.'); api.setBackgroundColor(arg_instanceId!, arg_color!); return {}; }); @@ -625,12 +574,8 @@ abstract class TestWebViewHostApi { } } -class _TestWebSettingsHostApiCodec extends StandardMessageCodec { - const _TestWebSettingsHostApiCodec(); -} - abstract class TestWebSettingsHostApi { - static const MessageCodec codec = _TestWebSettingsHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId, int webViewInstanceId); void dispose(int instanceId); @@ -638,7 +583,7 @@ abstract class TestWebSettingsHostApi { 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 setUserAgentString(int instanceId, String? userAgentString); void setMediaPlaybackRequiresUserGesture(int instanceId, bool require); void setSupportZoom(int instanceId, bool support); void setLoadWithOverviewMode(int instanceId, bool overview); @@ -647,25 +592,20 @@ abstract class TestWebSettingsHostApi { void setBuiltInZoomControls(int instanceId, bool enabled); void setAllowFileAccess(int instanceId, bool enabled); void setGeolocationEnabled(int instanceId, bool enabled); - static void setup(TestWebSettingsHostApi? api, - {BinaryMessenger? binaryMessenger}) { + static void setup(TestWebSettingsHostApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.create', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.create', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.create was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.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.WebSettingsHostApi.create was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.create was null, expected non-null int.'); final int? arg_webViewInstanceId = (args[1] as int?); - assert(arg_webViewInstanceId != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.create was null, expected non-null int.'); + assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!, arg_webViewInstanceId!); return {}; }); @@ -673,18 +613,15 @@ abstract class TestWebSettingsHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.dispose', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.dispose', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.dispose was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.dispose 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.WebSettingsHostApi.dispose was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.dispose was null, expected non-null int.'); api.dispose(arg_instanceId!); return {}; }); @@ -692,21 +629,17 @@ abstract class TestWebSettingsHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled 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.WebSettingsHostApi.setDomStorageEnabled was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled was null, expected non-null int.'); final bool? arg_flag = (args[1] as bool?); - assert(arg_flag != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled was null, expected non-null bool.'); + assert(arg_flag != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled was null, expected non-null bool.'); api.setDomStorageEnabled(arg_instanceId!, arg_flag!); return {}; }); @@ -714,46 +647,35 @@ abstract class TestWebSettingsHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically', - codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically 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.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically was null, expected non-null int.'); final bool? arg_flag = (args[1] as bool?); - assert(arg_flag != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically was null, expected non-null bool.'); - api.setJavaScriptCanOpenWindowsAutomatically( - arg_instanceId!, arg_flag!); + assert(arg_flag != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically was null, expected non-null bool.'); + api.setJavaScriptCanOpenWindowsAutomatically(arg_instanceId!, arg_flag!); return {}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows', - codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows 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.WebSettingsHostApi.setSupportMultipleWindows was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows was null, expected non-null int.'); final bool? arg_support = (args[1] as bool?); - assert(arg_support != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows was null, expected non-null bool.'); + assert(arg_support != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows was null, expected non-null bool.'); api.setSupportMultipleWindows(arg_instanceId!, arg_support!); return {}; }); @@ -761,21 +683,17 @@ abstract class TestWebSettingsHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled 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.WebSettingsHostApi.setJavaScriptEnabled was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled was null, expected non-null int.'); final bool? arg_flag = (args[1] as bool?); - assert(arg_flag != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled was null, expected non-null bool.'); + assert(arg_flag != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled was null, expected non-null bool.'); api.setJavaScriptEnabled(arg_instanceId!, arg_flag!); return {}; }); @@ -783,67 +701,52 @@ abstract class TestWebSettingsHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString 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.WebSettingsHostApi.setUserAgentString was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString was null, expected non-null int.'); final String? arg_userAgentString = (args[1] as String?); - assert(arg_userAgentString != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString was null, expected non-null String.'); - api.setUserAgentString(arg_instanceId!, arg_userAgentString!); + api.setUserAgentString(arg_instanceId!, arg_userAgentString); return {}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture', - codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture 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.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture was null, expected non-null int.'); final bool? arg_require = (args[1] as bool?); - assert(arg_require != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture was null, expected non-null bool.'); - api.setMediaPlaybackRequiresUserGesture( - arg_instanceId!, arg_require!); + assert(arg_require != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture was null, expected non-null bool.'); + api.setMediaPlaybackRequiresUserGesture(arg_instanceId!, arg_require!); return {}; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom 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.WebSettingsHostApi.setSupportZoom was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom was null, expected non-null int.'); final bool? arg_support = (args[1] as bool?); - assert(arg_support != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom was null, expected non-null bool.'); + assert(arg_support != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom was null, expected non-null bool.'); api.setSupportZoom(arg_instanceId!, arg_support!); return {}; }); @@ -851,22 +754,17 @@ abstract class TestWebSettingsHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode', - codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode 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.WebSettingsHostApi.setLoadWithOverviewMode was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode was null, expected non-null int.'); final bool? arg_overview = (args[1] as bool?); - assert(arg_overview != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode was null, expected non-null bool.'); + assert(arg_overview != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode was null, expected non-null bool.'); api.setLoadWithOverviewMode(arg_instanceId!, arg_overview!); return {}; }); @@ -874,21 +772,17 @@ abstract class TestWebSettingsHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort 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.WebSettingsHostApi.setUseWideViewPort was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort was null, expected non-null int.'); final bool? arg_use = (args[1] as bool?); - assert(arg_use != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort was null, expected non-null bool.'); + assert(arg_use != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort was null, expected non-null bool.'); api.setUseWideViewPort(arg_instanceId!, arg_use!); return {}; }); @@ -896,21 +790,17 @@ abstract class TestWebSettingsHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls 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.WebSettingsHostApi.setDisplayZoomControls was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls was null, expected non-null int.'); final bool? arg_enabled = (args[1] as bool?); - assert(arg_enabled != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls was null, expected non-null bool.'); + assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls was null, expected non-null bool.'); api.setDisplayZoomControls(arg_instanceId!, arg_enabled!); return {}; }); @@ -918,21 +808,17 @@ abstract class TestWebSettingsHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls 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.WebSettingsHostApi.setBuiltInZoomControls was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls was null, expected non-null int.'); final bool? arg_enabled = (args[1] as bool?); - assert(arg_enabled != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls was null, expected non-null bool.'); + assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls was null, expected non-null bool.'); api.setBuiltInZoomControls(arg_instanceId!, arg_enabled!); return {}; }); @@ -940,21 +826,17 @@ abstract class TestWebSettingsHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess 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.WebSettingsHostApi.setAllowFileAccess was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess was null, expected non-null int.'); final bool? arg_enabled = (args[1] as bool?); - assert(arg_enabled != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess was null, expected non-null bool.'); + assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess was null, expected non-null bool.'); api.setAllowFileAccess(arg_instanceId!, arg_enabled!); return {}; }); @@ -962,21 +844,17 @@ abstract class TestWebSettingsHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.setGeolocationEnabled', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebSettingsHostApi.setGeolocationEnabled', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setGeolocationEnabled was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setGeolocationEnabled 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.WebSettingsHostApi.setGeolocationEnabled was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setGeolocationEnabled was null, expected non-null int.'); final bool? arg_enabled = (args[1] as bool?); - assert(arg_enabled != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setGeolocationEnabled was null, expected non-null bool.'); + assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setGeolocationEnabled was null, expected non-null bool.'); api.setGeolocationEnabled(arg_instanceId!, arg_enabled!); return {}; }); @@ -985,34 +863,24 @@ abstract class TestWebSettingsHostApi { } } -class _TestJavaScriptChannelHostApiCodec extends StandardMessageCodec { - const _TestJavaScriptChannelHostApiCodec(); -} - abstract class TestJavaScriptChannelHostApi { - static const MessageCodec codec = - _TestJavaScriptChannelHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId, String channelName); - static void setup(TestJavaScriptChannelHostApi? api, - {BinaryMessenger? binaryMessenger}) { + static void setup(TestJavaScriptChannelHostApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaScriptChannelHostApi.create', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.JavaScriptChannelHostApi.create', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.create was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.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.JavaScriptChannelHostApi.create was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.create was null, expected non-null int.'); final String? arg_channelName = (args[1] as String?); - assert(arg_channelName != null, - 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.create was null, expected non-null String.'); + assert(arg_channelName != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.create was null, expected non-null String.'); api.create(arg_instanceId!, arg_channelName!); return {}; }); @@ -1021,33 +889,24 @@ abstract class TestJavaScriptChannelHostApi { } } -class _TestWebViewClientHostApiCodec extends StandardMessageCodec { - const _TestWebViewClientHostApiCodec(); -} - abstract class TestWebViewClientHostApi { - static const MessageCodec codec = _TestWebViewClientHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId, bool shouldOverrideUrlLoading); - static void setup(TestWebViewClientHostApi? api, - {BinaryMessenger? binaryMessenger}) { + static void setup(TestWebViewClientHostApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientHostApi.create', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebViewClientHostApi.create', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewClientHostApi.create was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebViewClientHostApi.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.WebViewClientHostApi.create was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientHostApi.create was null, expected non-null int.'); final bool? arg_shouldOverrideUrlLoading = (args[1] as bool?); - assert(arg_shouldOverrideUrlLoading != null, - 'Argument for dev.flutter.pigeon.WebViewClientHostApi.create was null, expected non-null bool.'); + assert(arg_shouldOverrideUrlLoading != null, 'Argument for dev.flutter.pigeon.WebViewClientHostApi.create was null, expected non-null bool.'); api.create(arg_instanceId!, arg_shouldOverrideUrlLoading!); return {}; }); @@ -1056,31 +915,22 @@ abstract class TestWebViewClientHostApi { } } -class _TestDownloadListenerHostApiCodec extends StandardMessageCodec { - const _TestDownloadListenerHostApiCodec(); -} - abstract class TestDownloadListenerHostApi { - static const MessageCodec codec = - _TestDownloadListenerHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId); - static void setup(TestDownloadListenerHostApi? api, - {BinaryMessenger? binaryMessenger}) { + static void setup(TestDownloadListenerHostApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DownloadListenerHostApi.create', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.DownloadListenerHostApi.create', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.DownloadListenerHostApi.create was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.DownloadListenerHostApi.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.DownloadListenerHostApi.create was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.DownloadListenerHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!); return {}; }); @@ -1089,33 +939,24 @@ abstract class TestDownloadListenerHostApi { } } -class _TestWebChromeClientHostApiCodec extends StandardMessageCodec { - const _TestWebChromeClientHostApiCodec(); -} - abstract class TestWebChromeClientHostApi { - static const MessageCodec codec = _TestWebChromeClientHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId, int webViewClientInstanceId); - static void setup(TestWebChromeClientHostApi? api, - {BinaryMessenger? binaryMessenger}) { + static void setup(TestWebChromeClientHostApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebChromeClientHostApi.create', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.WebChromeClientHostApi.create', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.create was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.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.WebChromeClientHostApi.create was null, expected non-null int.'); + assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.create was null, expected non-null int.'); final int? arg_webViewClientInstanceId = (args[1] as int?); - assert(arg_webViewClientInstanceId != null, - 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.create was null, expected non-null int.'); + assert(arg_webViewClientInstanceId != null, 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!, arg_webViewClientInstanceId!); return {}; }); @@ -1124,31 +965,23 @@ abstract class TestWebChromeClientHostApi { } } -class _TestAssetManagerHostApiCodec extends StandardMessageCodec { - const _TestAssetManagerHostApiCodec(); -} - abstract class TestAssetManagerHostApi { - static const MessageCodec codec = _TestAssetManagerHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); List list(String path); String getAssetFilePathByName(String name); - static void setup(TestAssetManagerHostApi? api, - {BinaryMessenger? binaryMessenger}) { + static void setup(TestAssetManagerHostApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.list was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.list was null.'); final List args = (message as List?)!; final String? arg_path = (args[0] as String?); - assert(arg_path != null, - 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.list was null, expected non-null String.'); + 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}; }); @@ -1156,19 +989,15 @@ abstract class TestAssetManagerHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName', - codec, - binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName', codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMockMessageHandler(null); } else { channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName was null.'); + assert(message != null, 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName was null.'); final List args = (message as List?)!; final String? arg_name = (args[0] as String?); - assert(arg_name != null, - 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName was null, expected non-null String.'); + 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}; }); @@ -1176,3 +1005,44 @@ abstract class TestAssetManagerHostApi { } } } + +abstract class TestWebStorageHostApi { + static const MessageCodec codec = StandardMessageCodec(); + + void create(int instanceId); + void deleteAllData(int instanceId); + static void setup(TestWebStorageHostApi? api, {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebStorageHostApi.create', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.WebStorageHostApi.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.WebStorageHostApi.create was null, expected non-null int.'); + api.create(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebStorageHostApi.deleteAllData', codec, binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, 'Argument for dev.flutter.pigeon.WebStorageHostApi.deleteAllData 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.WebStorageHostApi.deleteAllData was null, expected non-null int.'); + api.deleteAllData(arg_instanceId!); + return {}; + }); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart index 26753bc15979..d5eba097310d 100644 --- a/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart @@ -1,12 +1,14 @@ -// Mocks generated by Mockito 5.0.16 from annotations -// in webview_flutter_android/test/webview_android_cookie_manager_test.dart. +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_pro_android/test/webview_android_cookie_manager_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_pro_android/src/android_webview.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 @@ -15,6 +17,7 @@ import 'package:webview_pro_android/src/android_webview.dart' as _i2; // 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 [CookieManager]. /// @@ -25,14 +28,27 @@ class MockCookieManager extends _i1.Mock implements _i2.CookieManager { } @override - _i3.Future setCookie(String? url, String? value) => - (super.noSuchMethod(Invocation.method(#setCookie, [url, value]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + _i3.Future setCookie( + String? url, + String? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setCookie, + [ + url, + value, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i3.Future clearCookies() => - (super.noSuchMethod(Invocation.method(#clearCookies, []), - returnValue: Future.value(false)) as _i3.Future); - @override - String toString() => super.toString(); + _i3.Future clearCookies() => (super.noSuchMethod( + Invocation.method( + #clearCookies, + [], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); } diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart index fc059b3adc35..9d90614e1cba 100644 --- a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart @@ -16,14 +16,16 @@ import 'package:webview_pro_android/src/instance_manager.dart'; import 'package:webview_pro_android/webview_android_widget.dart'; import 'package:webview_pro_platform_interface/webview_flutter_platform_interface.dart'; -import 'android_webview.pigeon.dart'; import 'android_webview_test.mocks.dart' show MockTestWebViewHostApi; +import 'test_android_webview.pigeon.dart'; import 'webview_android_widget_test.mocks.dart'; @GenerateMocks([ android_webview.FlutterAssetManager, android_webview.WebSettings, + android_webview.WebStorage, android_webview.WebView, + android_webview.WebResourceRequest, WebViewAndroidDownloadListener, WebViewAndroidJavaScriptChannel, WebViewAndroidWebChromeClient, @@ -39,6 +41,7 @@ void main() { late MockFlutterAssetManager mockFlutterAssetManager; late MockWebView mockWebView; late MockWebSettings mockWebSettings; + late MockWebStorage mockWebStorage; late MockWebViewProxy mockWebViewProxy; late MockWebViewPlatformCallbacksHandler mockCallbacksHandler; @@ -54,6 +57,7 @@ void main() { mockFlutterAssetManager = MockFlutterAssetManager(); mockWebView = MockWebView(); mockWebSettings = MockWebSettings(); + mockWebStorage = MockWebStorage(); when(mockWebView.settings).thenReturn(mockWebSettings); mockWebViewProxy = MockWebViewProxy(); @@ -86,6 +90,7 @@ void main() { javascriptChannelRegistry: mockJavascriptChannelRegistry, webViewProxy: mockWebViewProxy, flutterAssetManager: mockFlutterAssetManager, + webStorage: mockWebStorage, onBuildWidget: (WebViewAndroidPlatformController controller) { testController = controller; return Container(); @@ -160,8 +165,6 @@ void main() { await buildWidget( tester, creationParams: CreationParams( - autoMediaPlaybackPolicy: - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, webSettings: WebSettings( userAgent: const WebSetting.absent(), hasNavigationDelegate: false, @@ -590,6 +593,7 @@ void main() { await testController.clearCache(); verify(mockWebView.clearCache(true)); + verify(mockWebStorage.deleteAllData()); }); testWidgets('evaluateJavascript', (WidgetTester tester) async { @@ -822,7 +826,7 @@ void main() { // of WebView itstelf. mockPlatformHostApi = MockTestWebViewHostApi(); TestWebViewHostApi.setup(mockPlatformHostApi); - instanceManager = InstanceManager(); + instanceManager = InstanceManager(onWeakReferenceRemoved: (_) {}); android_webview.WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); }); @@ -839,4 +843,192 @@ void main() { verify(mockPlatformHostApi.setWebContentsDebuggingEnabled(false)); }); }); + + group('WebViewAndroidWebViewClient', () { + test( + 'urlLoading should call loadUrl when onNavigationRequestCallback returns true', + () { + final Completer completer = Completer(); + final WebViewAndroidWebViewClient webViewClient = + WebViewAndroidWebViewClient.handlesNavigation( + onPageStartedCallback: (_) {}, + onPageFinishedCallback: (_) {}, + onWebResourceErrorCallback: (_) {}, + onNavigationRequestCallback: ({ + required bool isForMainFrame, + required String url, + }) => + true, + loadUrl: (String url, Map? headers) async { + completer.complete(); + }); + + webViewClient.urlLoading(MockWebView(), 'https://flutter.dev'); + expect(completer.isCompleted, isTrue); + }); + + test( + 'urlLoading should call loadUrl when onNavigationRequestCallback returns a Future true', + () async { + final Completer completer = Completer(); + final WebViewAndroidWebViewClient webViewClient = + WebViewAndroidWebViewClient.handlesNavigation( + onPageStartedCallback: (_) {}, + onPageFinishedCallback: (_) {}, + onWebResourceErrorCallback: (_) {}, + onNavigationRequestCallback: ({ + required bool isForMainFrame, + required String url, + }) => + Future.value(true), + loadUrl: (String url, Map? headers) async { + completer.complete(); + }); + + webViewClient.urlLoading(MockWebView(), 'https://flutter.dev'); + expect(completer.future, completes); + }); + + test( + 'urlLoading should not call laodUrl when onNavigationRequestCallback returns false', + () async { + final WebViewAndroidWebViewClient webViewClient = + WebViewAndroidWebViewClient.handlesNavigation( + onPageStartedCallback: (_) {}, + onPageFinishedCallback: (_) {}, + onWebResourceErrorCallback: (_) {}, + onNavigationRequestCallback: ({ + required bool isForMainFrame, + required String url, + }) => + false, + loadUrl: (String url, Map? headers) async { + fail( + 'loadUrl should not be called if onNavigationRequestCallback returns false.'); + }); + + webViewClient.urlLoading(MockWebView(), 'https://flutter.dev'); + }); + + test( + 'urlLoading should not call loadUrl when onNavigationRequestCallback returns a Future false', + () { + final WebViewAndroidWebViewClient webViewClient = + WebViewAndroidWebViewClient.handlesNavigation( + onPageStartedCallback: (_) {}, + onPageFinishedCallback: (_) {}, + onWebResourceErrorCallback: (_) {}, + onNavigationRequestCallback: ({ + required bool isForMainFrame, + required String url, + }) => + Future.value(false), + loadUrl: (String url, Map? headers) async { + fail( + 'loadUrl should not be called if onNavigationRequestCallback returns false.'); + }); + + webViewClient.urlLoading(MockWebView(), 'https://flutter.dev'); + }); + + test( + 'requestLoading should call loadUrl when onNavigationRequestCallback returns true', + () { + final Completer completer = Completer(); + final MockWebResourceRequest mockRequest = MockWebResourceRequest(); + when(mockRequest.isForMainFrame).thenReturn(true); + when(mockRequest.url).thenReturn('https://flutter.dev'); + final WebViewAndroidWebViewClient webViewClient = + WebViewAndroidWebViewClient.handlesNavigation( + onPageStartedCallback: (_) {}, + onPageFinishedCallback: (_) {}, + onWebResourceErrorCallback: (_) {}, + onNavigationRequestCallback: ({ + required bool isForMainFrame, + required String url, + }) => + true, + loadUrl: (String url, Map? headers) async { + expect(url, 'https://flutter.dev'); + completer.complete(); + }); + + webViewClient.requestLoading(MockWebView(), mockRequest); + expect(completer.isCompleted, isTrue); + }); + + test( + 'requestLoading should call loadUrl when onNavigationRequestCallback returns a Future true', + () async { + final Completer completer = Completer(); + final MockWebResourceRequest mockRequest = MockWebResourceRequest(); + when(mockRequest.isForMainFrame).thenReturn(true); + when(mockRequest.url).thenReturn('https://flutter.dev'); + final WebViewAndroidWebViewClient webViewClient = + WebViewAndroidWebViewClient.handlesNavigation( + onPageStartedCallback: (_) {}, + onPageFinishedCallback: (_) {}, + onWebResourceErrorCallback: (_) {}, + onNavigationRequestCallback: ({ + required bool isForMainFrame, + required String url, + }) => + Future.value(true), + loadUrl: (String url, Map? headers) async { + expect(url, 'https://flutter.dev'); + completer.complete(); + }); + + webViewClient.requestLoading(MockWebView(), mockRequest); + expect(completer.future, completes); + }); + + test( + 'requestLoading should not call loadUrl when onNavigationRequestCallback returns false', + () { + final MockWebResourceRequest mockRequest = MockWebResourceRequest(); + when(mockRequest.isForMainFrame).thenReturn(true); + when(mockRequest.url).thenReturn('https://flutter.dev'); + final WebViewAndroidWebViewClient webViewClient = + WebViewAndroidWebViewClient.handlesNavigation( + onPageStartedCallback: (_) {}, + onPageFinishedCallback: (_) {}, + onWebResourceErrorCallback: (_) {}, + onNavigationRequestCallback: ({ + required bool isForMainFrame, + required String url, + }) => + false, + loadUrl: (String url, Map? headers) { + fail( + 'loadUrl should not be called if onNavigationRequestCallback returns false.'); + }); + + webViewClient.requestLoading(MockWebView(), mockRequest); + }); + + test( + 'requestLoading should not call loadUrl when onNavigationRequestCallback returns a Future false', + () { + final MockWebResourceRequest mockRequest = MockWebResourceRequest(); + when(mockRequest.isForMainFrame).thenReturn(true); + when(mockRequest.url).thenReturn('https://flutter.dev'); + final WebViewAndroidWebViewClient webViewClient = + WebViewAndroidWebViewClient.handlesNavigation( + onPageStartedCallback: (_) {}, + onPageFinishedCallback: (_) {}, + onWebResourceErrorCallback: (_) {}, + onNavigationRequestCallback: ({ + required bool isForMainFrame, + required String url, + }) => + Future.value(false), + loadUrl: (String url, Map? headers) { + fail( + 'loadUrl should not be called if onNavigationRequestCallback returns false.'); + }); + + webViewClient.requestLoading(MockWebView(), mockRequest); + }); + }); } diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart index 904854e01682..c9f4bcd98f17 100644 --- a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart @@ -1,17 +1,19 @@ -// Mocks generated by Mockito 5.0.16 from annotations -// in webview_flutter_android/test/webview_android_widget_test.dart. +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_pro_android/test/webview_android_widget_test.dart. // Do not manually edit this file. -import 'dart:async' as _i4; -import 'dart:typed_data' as _i5; -import 'dart:ui' as _i6; +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; +import 'dart:typed_data' as _i6; +import 'dart:ui' as _i3; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_pro_android/src/android_webview.dart' as _i2; import 'package:webview_pro_android/webview_android_widget.dart' as _i7; import 'package:webview_pro_platform_interface/webview_flutter_platform_interface.dart' - as _i3; + as _i4; +// ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references @@ -20,13 +22,101 @@ import 'package:webview_pro_platform_interface/webview_flutter_platform_interfac // 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 _FakeWebSettings_0 extends _i1.Fake implements _i2.WebSettings {} +class _FakeWebSettings_0 extends _i1.SmartFake implements _i2.WebSettings { + _FakeWebSettings_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebStorage_1 extends _i1.SmartFake implements _i2.WebStorage { + _FakeWebStorage_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeOffset_2 extends _i1.SmartFake implements _i3.Offset { + _FakeOffset_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebView_3 extends _i1.SmartFake implements _i2.WebView { + _FakeWebView_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDownloadListener_4 extends _i1.SmartFake + implements _i2.DownloadListener { + _FakeDownloadListener_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeJavascriptChannelRegistry_1 extends _i1.Fake - implements _i3.JavascriptChannelRegistry {} +class _FakeJavascriptChannelRegistry_5 extends _i1.SmartFake + implements _i4.JavascriptChannelRegistry { + _FakeJavascriptChannelRegistry_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeWebView_2 extends _i1.Fake implements _i2.WebView {} +class _FakeJavaScriptChannel_6 extends _i1.SmartFake + implements _i2.JavaScriptChannel { + _FakeJavaScriptChannel_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebChromeClient_7 extends _i1.SmartFake + implements _i2.WebChromeClient { + _FakeWebChromeClient_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebViewClient_8 extends _i1.SmartFake implements _i2.WebViewClient { + _FakeWebViewClient_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} /// A class which mocks [FlutterAssetManager]. /// @@ -38,16 +128,22 @@ class MockFlutterAssetManager extends _i1.Mock } @override - _i4.Future> list(String? path) => - (super.noSuchMethod(Invocation.method(#list, [path]), - returnValue: Future>.value([])) - as _i4.Future>); + _i5.Future> list(String? path) => (super.noSuchMethod( + Invocation.method( + #list, + [path], + ), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future getAssetFilePathByName(String? name) => - (super.noSuchMethod(Invocation.method(#getAssetFilePathByName, [name]), - returnValue: Future.value('')) as _i4.Future); - @override - String toString() => super.toString(); + _i5.Future getAssetFilePathByName(String? name) => + (super.noSuchMethod( + Invocation.method( + #getAssetFilePathByName, + [name], + ), + returnValue: _i5.Future.value(''), + ) as _i5.Future); } /// A class which mocks [WebSettings]. @@ -59,69 +155,174 @@ class MockWebSettings extends _i1.Mock implements _i2.WebSettings { } @override - _i4.Future setDomStorageEnabled(bool? flag) => - (super.noSuchMethod(Invocation.method(#setDomStorageEnabled, [flag]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + _i5.Future setDomStorageEnabled(bool? flag) => (super.noSuchMethod( + Invocation.method( + #setDomStorageEnabled, + [flag], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future setJavaScriptCanOpenWindowsAutomatically(bool? flag) => + _i5.Future setJavaScriptCanOpenWindowsAutomatically(bool? flag) => + (super.noSuchMethod( + Invocation.method( + #setJavaScriptCanOpenWindowsAutomatically, + [flag], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setSupportMultipleWindows(bool? support) => (super.noSuchMethod( - Invocation.method(#setJavaScriptCanOpenWindowsAutomatically, [flag]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setSupportMultipleWindows(bool? support) => (super - .noSuchMethod(Invocation.method(#setSupportMultipleWindows, [support]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setJavaScriptEnabled(bool? flag) => - (super.noSuchMethod(Invocation.method(#setJavaScriptEnabled, [flag]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setUserAgentString(String? userAgentString) => (super - .noSuchMethod(Invocation.method(#setUserAgentString, [userAgentString]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setMediaPlaybackRequiresUserGesture(bool? require) => + Invocation.method( + #setSupportMultipleWindows, + [support], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setJavaScriptEnabled(bool? flag) => (super.noSuchMethod( + Invocation.method( + #setJavaScriptEnabled, + [flag], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setUserAgentString(String? userAgentString) => (super.noSuchMethod( - Invocation.method(#setMediaPlaybackRequiresUserGesture, [require]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setSupportZoom(bool? support) => - (super.noSuchMethod(Invocation.method(#setSupportZoom, [support]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setLoadWithOverviewMode(bool? overview) => (super - .noSuchMethod(Invocation.method(#setLoadWithOverviewMode, [overview]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setUseWideViewPort(bool? use) => - (super.noSuchMethod(Invocation.method(#setUseWideViewPort, [use]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setDisplayZoomControls(bool? enabled) => - (super.noSuchMethod(Invocation.method(#setDisplayZoomControls, [enabled]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setBuiltInZoomControls(bool? enabled) => - (super.noSuchMethod(Invocation.method(#setBuiltInZoomControls, [enabled]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setAllowFileAccess(bool? enabled) => - (super.noSuchMethod(Invocation.method(#setAllowFileAccess, [enabled]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - String toString() => super.toString(); + Invocation.method( + #setUserAgentString, + [userAgentString], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setMediaPlaybackRequiresUserGesture(bool? require) => + (super.noSuchMethod( + Invocation.method( + #setMediaPlaybackRequiresUserGesture, + [require], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setSupportZoom(bool? support) => (super.noSuchMethod( + Invocation.method( + #setSupportZoom, + [support], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setGeolocation(bool? support) => (super.noSuchMethod( + Invocation.method( + #setGeolocation, + [support], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setLoadWithOverviewMode(bool? overview) => + (super.noSuchMethod( + Invocation.method( + #setLoadWithOverviewMode, + [overview], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setUseWideViewPort(bool? use) => (super.noSuchMethod( + Invocation.method( + #setUseWideViewPort, + [use], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setDisplayZoomControls(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #setDisplayZoomControls, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setBuiltInZoomControls(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #setBuiltInZoomControls, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setAllowFileAccess(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #setAllowFileAccess, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i2.WebSettings copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebSettings_0( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebSettings); +} + +/// A class which mocks [WebStorage]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebStorage extends _i1.Mock implements _i2.WebStorage { + MockWebStorage() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future deleteAllData() => (super.noSuchMethod( + Invocation.method( + #deleteAllData, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i2.WebStorage copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebStorage_1( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebStorage); } /// A class which mocks [WebView]. @@ -133,147 +334,351 @@ class MockWebView extends _i1.Mock implements _i2.WebView { } @override - bool get useHybridComposition => - (super.noSuchMethod(Invocation.getter(#useHybridComposition), - returnValue: false) as bool); - @override - _i2.WebSettings get settings => - (super.noSuchMethod(Invocation.getter(#settings), - returnValue: _FakeWebSettings_0()) as _i2.WebSettings); - @override - _i4.Future loadData( - {String? data, String? mimeType, String? encoding}) => + bool get useHybridComposition => (super.noSuchMethod( + Invocation.getter(#useHybridComposition), + returnValue: false, + ) as bool); + @override + _i2.WebSettings get settings => (super.noSuchMethod( + Invocation.getter(#settings), + returnValue: _FakeWebSettings_0( + this, + Invocation.getter(#settings), + ), + ) as _i2.WebSettings); + @override + _i5.Future loadData({ + required String? data, + String? mimeType, + String? encoding, + }) => (super.noSuchMethod( - Invocation.method(#loadData, [], - {#data: data, #mimeType: mimeType, #encoding: encoding}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future loadDataWithBaseUrl( - {String? baseUrl, - String? data, - String? mimeType, - String? encoding, - String? historyUrl}) => + Invocation.method( + #loadData, + [], + { + #data: data, + #mimeType: mimeType, + #encoding: encoding, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadDataWithBaseUrl({ + String? baseUrl, + required String? data, + String? mimeType, + String? encoding, + String? historyUrl, + }) => (super.noSuchMethod( - Invocation.method(#loadDataWithBaseUrl, [], { + Invocation.method( + #loadDataWithBaseUrl, + [], + { #baseUrl: baseUrl, #data: data, #mimeType: mimeType, #encoding: encoding, - #historyUrl: historyUrl - }), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future loadUrl(String? url, Map? headers) => - (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future postUrl(String? url, _i5.Uint8List? data) => - (super.noSuchMethod(Invocation.method(#postUrl, [url, data]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future getUrl() => - (super.noSuchMethod(Invocation.method(#getUrl, []), - returnValue: Future.value()) as _i4.Future); - @override - _i4.Future canGoBack() => - (super.noSuchMethod(Invocation.method(#canGoBack, []), - returnValue: Future.value(false)) as _i4.Future); - @override - _i4.Future canGoForward() => - (super.noSuchMethod(Invocation.method(#canGoForward, []), - returnValue: Future.value(false)) as _i4.Future); - @override - _i4.Future goBack() => - (super.noSuchMethod(Invocation.method(#goBack, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future goForward() => - (super.noSuchMethod(Invocation.method(#goForward, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future reload() => - (super.noSuchMethod(Invocation.method(#reload, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future clearCache(bool? includeDiskFiles) => - (super.noSuchMethod(Invocation.method(#clearCache, [includeDiskFiles]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future evaluateJavascript(String? javascriptString) => (super - .noSuchMethod(Invocation.method(#evaluateJavascript, [javascriptString]), - returnValue: Future.value()) as _i4.Future); - @override - _i4.Future getTitle() => - (super.noSuchMethod(Invocation.method(#getTitle, []), - returnValue: Future.value()) as _i4.Future); - @override - _i4.Future scrollTo(int? x, int? y) => - (super.noSuchMethod(Invocation.method(#scrollTo, [x, y]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future scrollBy(int? x, int? y) => - (super.noSuchMethod(Invocation.method(#scrollBy, [x, y]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future getScrollX() => - (super.noSuchMethod(Invocation.method(#getScrollX, []), - returnValue: Future.value(0)) as _i4.Future); - @override - _i4.Future getScrollY() => - (super.noSuchMethod(Invocation.method(#getScrollY, []), - returnValue: Future.value(0)) as _i4.Future); - @override - _i4.Future setWebViewClient(_i2.WebViewClient? webViewClient) => - (super.noSuchMethod(Invocation.method(#setWebViewClient, [webViewClient]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future addJavaScriptChannel( - _i2.JavaScriptChannel? javaScriptChannel) => + #historyUrl: historyUrl, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadUrl( + String? url, + Map? headers, + ) => + (super.noSuchMethod( + Invocation.method( + #loadUrl, + [ + url, + headers, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future postUrl( + String? url, + _i6.Uint8List? data, + ) => + (super.noSuchMethod( + Invocation.method( + #postUrl, + [ + url, + data, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getUrl() => (super.noSuchMethod( + Invocation.method( + #getUrl, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future clearCache(bool? includeDiskFiles) => (super.noSuchMethod( + Invocation.method( + #clearCache, + [includeDiskFiles], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future evaluateJavascript(String? javascriptString) => (super.noSuchMethod( - Invocation.method(#addJavaScriptChannel, [javaScriptChannel]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + Invocation.method( + #evaluateJavascript, + [javascriptString], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future scrollTo( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + x, + y, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future scrollBy( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + x, + y, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getScrollX() => (super.noSuchMethod( + Invocation.method( + #getScrollX, + [], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future getScrollY() => (super.noSuchMethod( + Invocation.method( + #getScrollY, + [], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future<_i3.Offset> getScrollPosition() => (super.noSuchMethod( + Invocation.method( + #getScrollPosition, + [], + ), + returnValue: _i5.Future<_i3.Offset>.value(_FakeOffset_2( + this, + Invocation.method( + #getScrollPosition, + [], + ), + )), + ) as _i5.Future<_i3.Offset>); @override - _i4.Future removeJavaScriptChannel( + _i5.Future setWebViewClient(_i2.WebViewClient? webViewClient) => + (super.noSuchMethod( + Invocation.method( + #setWebViewClient, + [webViewClient], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future addJavaScriptChannel( + _i2.JavaScriptChannel? javaScriptChannel) => + (super.noSuchMethod( + Invocation.method( + #addJavaScriptChannel, + [javaScriptChannel], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeJavaScriptChannel( _i2.JavaScriptChannel? javaScriptChannel) => (super.noSuchMethod( - Invocation.method(#removeJavaScriptChannel, [javaScriptChannel]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setDownloadListener(_i2.DownloadListener? listener) => - (super.noSuchMethod(Invocation.method(#setDownloadListener, [listener]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setWebChromeClient(_i2.WebChromeClient? client) => - (super.noSuchMethod(Invocation.method(#setWebChromeClient, [client]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setBackgroundColor(_i6.Color? color) => - (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future release() => - (super.noSuchMethod(Invocation.method(#release, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - String toString() => super.toString(); + Invocation.method( + #removeJavaScriptChannel, + [javaScriptChannel], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setDownloadListener(_i2.DownloadListener? listener) => + (super.noSuchMethod( + Invocation.method( + #setDownloadListener, + [listener], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setWebChromeClient(_i2.WebChromeClient? client) => + (super.noSuchMethod( + Invocation.method( + #setWebChromeClient, + [client], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setBackgroundColor(_i3.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future release() => (super.noSuchMethod( + Invocation.method( + #release, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i2.WebView copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebView_3( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebView); +} + +/// A class which mocks [WebResourceRequest]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebResourceRequest extends _i1.Mock + implements _i2.WebResourceRequest { + MockWebResourceRequest() { + _i1.throwOnMissingStub(this); + } + + @override + String get url => (super.noSuchMethod( + Invocation.getter(#url), + returnValue: '', + ) as String); + @override + bool get isForMainFrame => (super.noSuchMethod( + Invocation.getter(#isForMainFrame), + returnValue: false, + ) as bool); + @override + bool get hasGesture => (super.noSuchMethod( + Invocation.getter(#hasGesture), + returnValue: false, + ) as bool); + @override + String get method => (super.noSuchMethod( + Invocation.getter(#method), + returnValue: '', + ) as String); + @override + Map get requestHeaders => (super.noSuchMethod( + Invocation.getter(#requestHeaders), + returnValue: {}, + ) as Map); } /// A class which mocks [WebViewAndroidDownloadListener]. @@ -286,20 +691,55 @@ class MockWebViewAndroidDownloadListener extends _i1.Mock } @override - _i4.Future Function(String, Map?) get loadUrl => - (super.noSuchMethod(Invocation.getter(#loadUrl), - returnValue: (String url, Map? headers) => - Future.value()) as _i4.Future Function( - String, Map?)); - @override - void onDownloadStart(String? url, String? userAgent, - String? contentDisposition, String? mimetype, int? contentLength) => + _i5.Future Function( + String, + Map?, + ) get loadUrl => (super.noSuchMethod( + Invocation.getter(#loadUrl), + returnValue: ( + String url, + Map? headers, + ) => + _i5.Future.value(), + ) as _i5.Future Function( + String, + Map?, + )); + @override + void onDownloadStart( + String? url, + String? userAgent, + String? contentDisposition, + String? mimetype, + int? contentLength, + ) => super.noSuchMethod( - Invocation.method(#onDownloadStart, - [url, userAgent, contentDisposition, mimetype, contentLength]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + Invocation.method( + #onDownloadStart, + [ + url, + userAgent, + contentDisposition, + mimetype, + contentLength, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i2.DownloadListener copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeDownloadListener_4( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.DownloadListener); } /// A class which mocks [WebViewAndroidJavaScriptChannel]. @@ -312,20 +752,41 @@ class MockWebViewAndroidJavaScriptChannel extends _i1.Mock } @override - _i3.JavascriptChannelRegistry get javascriptChannelRegistry => - (super.noSuchMethod(Invocation.getter(#javascriptChannelRegistry), - returnValue: _FakeJavascriptChannelRegistry_1()) - as _i3.JavascriptChannelRegistry); - @override - String get channelName => - (super.noSuchMethod(Invocation.getter(#channelName), returnValue: '') - as String); - @override - void postMessage(String? message) => - super.noSuchMethod(Invocation.method(#postMessage, [message]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + _i4.JavascriptChannelRegistry get javascriptChannelRegistry => + (super.noSuchMethod( + Invocation.getter(#javascriptChannelRegistry), + returnValue: _FakeJavascriptChannelRegistry_5( + this, + Invocation.getter(#javascriptChannelRegistry), + ), + ) as _i4.JavascriptChannelRegistry); + @override + String get channelName => (super.noSuchMethod( + Invocation.getter(#channelName), + returnValue: '', + ) as String); + @override + void postMessage(String? message) => super.noSuchMethod( + Invocation.method( + #postMessage, + [message], + ), + returnValueForMissingStub: null, + ); + @override + _i2.JavaScriptChannel copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeJavaScriptChannel_6( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.JavaScriptChannel); } /// A class which mocks [WebViewAndroidWebChromeClient]. @@ -338,11 +799,34 @@ class MockWebViewAndroidWebChromeClient extends _i1.Mock } @override - void onProgressChanged(_i2.WebView? webView, int? progress) => super - .noSuchMethod(Invocation.method(#onProgressChanged, [webView, progress]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + void onProgressChanged( + _i2.WebView? webView, + int? progress, + ) => + super.noSuchMethod( + Invocation.method( + #onProgressChanged, + [ + webView, + progress, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i2.WebChromeClient copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebChromeClient_7( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebChromeClient); } /// A class which mocks [WebViewAndroidWebViewClient]. @@ -355,127 +839,250 @@ class MockWebViewAndroidWebViewClient extends _i1.Mock } @override - void Function(String) get onPageStartedCallback => - (super.noSuchMethod(Invocation.getter(#onPageStartedCallback), - returnValue: (String url) {}) as void Function(String)); + void Function(String) get onPageStartedCallback => (super.noSuchMethod( + Invocation.getter(#onPageStartedCallback), + returnValue: (String url) {}, + ) as void Function(String)); @override - void Function(String) get onPageFinishedCallback => - (super.noSuchMethod(Invocation.getter(#onPageFinishedCallback), - returnValue: (String url) {}) as void Function(String)); + void Function(String) get onPageFinishedCallback => (super.noSuchMethod( + Invocation.getter(#onPageFinishedCallback), + returnValue: (String url) {}, + ) as void Function(String)); @override - void Function(_i3.WebResourceError) get onWebResourceErrorCallback => - (super.noSuchMethod(Invocation.getter(#onWebResourceErrorCallback), - returnValue: (_i3.WebResourceError error) {}) - as void Function(_i3.WebResourceError)); + void Function(_i4.WebResourceError) get onWebResourceErrorCallback => + (super.noSuchMethod( + Invocation.getter(#onWebResourceErrorCallback), + returnValue: (_i4.WebResourceError error) {}, + ) as void Function(_i4.WebResourceError)); @override set onWebResourceErrorCallback( - void Function(_i3.WebResourceError)? _onWebResourceErrorCallback) => + void Function(_i4.WebResourceError)? _onWebResourceErrorCallback) => super.noSuchMethod( - Invocation.setter( - #onWebResourceErrorCallback, _onWebResourceErrorCallback), - returnValueForMissingStub: null); - @override - bool get handlesNavigation => - (super.noSuchMethod(Invocation.getter(#handlesNavigation), - returnValue: false) as bool); - @override - bool get shouldOverrideUrlLoading => - (super.noSuchMethod(Invocation.getter(#shouldOverrideUrlLoading), - returnValue: false) as bool); - @override - void onPageStarted(_i2.WebView? webView, String? url) => - super.noSuchMethod(Invocation.method(#onPageStarted, [webView, url]), - returnValueForMissingStub: null); - @override - void onPageFinished(_i2.WebView? webView, String? url) => - super.noSuchMethod(Invocation.method(#onPageFinished, [webView, url]), - returnValueForMissingStub: null); - @override - void onReceivedError(_i2.WebView? webView, int? errorCode, - String? description, String? failingUrl) => + Invocation.setter( + #onWebResourceErrorCallback, + _onWebResourceErrorCallback, + ), + returnValueForMissingStub: null, + ); + @override + bool get handlesNavigation => (super.noSuchMethod( + Invocation.getter(#handlesNavigation), + returnValue: false, + ) as bool); + @override + bool get shouldOverrideUrlLoading => (super.noSuchMethod( + Invocation.getter(#shouldOverrideUrlLoading), + returnValue: false, + ) as bool); + @override + void onPageStarted( + _i2.WebView? webView, + String? url, + ) => super.noSuchMethod( - Invocation.method( - #onReceivedError, [webView, errorCode, description, failingUrl]), - returnValueForMissingStub: null); - @override - void onReceivedRequestError(_i2.WebView? webView, - _i2.WebResourceRequest? request, _i2.WebResourceError? error) => + Invocation.method( + #onPageStarted, + [ + webView, + url, + ], + ), + returnValueForMissingStub: null, + ); + @override + void onPageFinished( + _i2.WebView? webView, + String? url, + ) => super.noSuchMethod( - Invocation.method(#onReceivedRequestError, [webView, request, error]), - returnValueForMissingStub: null); - @override - void urlLoading(_i2.WebView? webView, String? url) => - super.noSuchMethod(Invocation.method(#urlLoading, [webView, url]), - returnValueForMissingStub: null); - @override - void requestLoading(_i2.WebView? webView, _i2.WebResourceRequest? request) => - super.noSuchMethod(Invocation.method(#requestLoading, [webView, request]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + Invocation.method( + #onPageFinished, + [ + webView, + url, + ], + ), + returnValueForMissingStub: null, + ); + @override + void onReceivedError( + _i2.WebView? webView, + int? errorCode, + String? description, + String? failingUrl, + ) => + super.noSuchMethod( + Invocation.method( + #onReceivedError, + [ + webView, + errorCode, + description, + failingUrl, + ], + ), + returnValueForMissingStub: null, + ); + @override + void onReceivedRequestError( + _i2.WebView? webView, + _i2.WebResourceRequest? request, + _i2.WebResourceError? error, + ) => + super.noSuchMethod( + Invocation.method( + #onReceivedRequestError, + [ + webView, + request, + error, + ], + ), + returnValueForMissingStub: null, + ); + @override + void urlLoading( + _i2.WebView? webView, + String? url, + ) => + super.noSuchMethod( + Invocation.method( + #urlLoading, + [ + webView, + url, + ], + ), + returnValueForMissingStub: null, + ); + @override + void requestLoading( + _i2.WebView? webView, + _i2.WebResourceRequest? request, + ) => + super.noSuchMethod( + Invocation.method( + #requestLoading, + [ + webView, + request, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i2.WebViewClient copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebViewClient_8( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebViewClient); } /// A class which mocks [JavascriptChannelRegistry]. /// /// See the documentation for Mockito's code generation for more information. class MockJavascriptChannelRegistry extends _i1.Mock - implements _i3.JavascriptChannelRegistry { + implements _i4.JavascriptChannelRegistry { MockJavascriptChannelRegistry() { _i1.throwOnMissingStub(this); } @override - Map get channels => - (super.noSuchMethod(Invocation.getter(#channels), - returnValue: {}) - as Map); + Map get channels => (super.noSuchMethod( + Invocation.getter(#channels), + returnValue: {}, + ) as Map); @override - void onJavascriptChannelMessage(String? channel, String? message) => + void onJavascriptChannelMessage( + String? channel, + String? message, + ) => super.noSuchMethod( - Invocation.method(#onJavascriptChannelMessage, [channel, message]), - returnValueForMissingStub: null); - @override - void updateJavascriptChannelsFromSet(Set<_i3.JavascriptChannel>? channels) => + Invocation.method( + #onJavascriptChannelMessage, + [ + channel, + message, + ], + ), + returnValueForMissingStub: null, + ); + @override + void updateJavascriptChannelsFromSet(Set<_i4.JavascriptChannel>? channels) => super.noSuchMethod( - Invocation.method(#updateJavascriptChannelsFromSet, [channels]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + Invocation.method( + #updateJavascriptChannelsFromSet, + [channels], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [WebViewPlatformCallbacksHandler]. /// /// See the documentation for Mockito's code generation for more information. class MockWebViewPlatformCallbacksHandler extends _i1.Mock - implements _i3.WebViewPlatformCallbacksHandler { + implements _i4.WebViewPlatformCallbacksHandler { MockWebViewPlatformCallbacksHandler() { _i1.throwOnMissingStub(this); } @override - _i4.FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) => + _i5.FutureOr onNavigationRequest({ + required String? url, + required bool? isForMainFrame, + }) => (super.noSuchMethod( - Invocation.method(#onNavigationRequest, [], - {#url: url, #isForMainFrame: isForMainFrame}), - returnValue: Future.value(false)) as _i4.FutureOr); - @override - void onPageStarted(String? url) => - super.noSuchMethod(Invocation.method(#onPageStarted, [url]), - returnValueForMissingStub: null); - @override - void onPageFinished(String? url) => - super.noSuchMethod(Invocation.method(#onPageFinished, [url]), - returnValueForMissingStub: null); - @override - void onProgress(int? progress) => - super.noSuchMethod(Invocation.method(#onProgress, [progress]), - returnValueForMissingStub: null); - @override - void onWebResourceError(_i3.WebResourceError? error) => - super.noSuchMethod(Invocation.method(#onWebResourceError, [error]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + Invocation.method( + #onNavigationRequest, + [], + { + #url: url, + #isForMainFrame: isForMainFrame, + }, + ), + returnValue: _i5.Future.value(false), + ) as _i5.FutureOr); + @override + void onPageStarted(String? url) => super.noSuchMethod( + Invocation.method( + #onPageStarted, + [url], + ), + returnValueForMissingStub: null, + ); + @override + void onPageFinished(String? url) => super.noSuchMethod( + Invocation.method( + #onPageFinished, + [url], + ), + returnValueForMissingStub: null, + ); + @override + void onProgress(int? progress) => super.noSuchMethod( + Invocation.method( + #onProgress, + [progress], + ), + returnValueForMissingStub: null, + ); + @override + void onWebResourceError(_i4.WebResourceError? error) => super.noSuchMethod( + Invocation.method( + #onWebResourceError, + [error], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [WebViewProxy]. @@ -487,17 +1094,30 @@ class MockWebViewProxy extends _i1.Mock implements _i7.WebViewProxy { } @override - _i2.WebView createWebView({bool? useHybridComposition}) => + _i2.WebView createWebView({required bool? useHybridComposition}) => (super.noSuchMethod( - Invocation.method(#createWebView, [], - {#useHybridComposition: useHybridComposition}), - returnValue: _FakeWebView_2()) as _i2.WebView); - @override - _i4.Future setWebContentsDebuggingEnabled(bool? enabled) => + Invocation.method( + #createWebView, + [], + {#useHybridComposition: useHybridComposition}, + ), + returnValue: _FakeWebView_3( + this, + Invocation.method( + #createWebView, + [], + {#useHybridComposition: useHybridComposition}, + ), + ), + ) as _i2.WebView); + @override + _i5.Future setWebContentsDebuggingEnabled(bool? enabled) => (super.noSuchMethod( - Invocation.method(#setWebContentsDebuggingEnabled, [enabled]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - String toString() => super.toString(); + Invocation.method( + #setWebContentsDebuggingEnabled, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index a3939092ad10..b27d705e089d 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,40 @@ +## 1.9.5+1 + +* Merge changes from upstream + +## 1.9.5 + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. + +## 1.9.4 + +* Updates imports for `prefer_relative_imports`. + +## 1.9.3 + +* Updates minimum Flutter version to 2.10. +* Removes `BuildParams` from v4 interface and adds `layoutDirection` to the creation params. + +## 1.9.2 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). +* Adds missing build params for v4 WebViewWidget interface. + +## 1.9.1 + +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/104231). + +## 1.9.0 + +* Adds the first iteration of the v4 webview_flutter interface implementation. +* Removes unnecessary imports. + +## 1.8.2 + +* Migrates from `ui.hash*` to `Object.hash*`. +* Updates minimum Flutter version to 2.5.0. + ## 1.8.1+2 * fix flutter analyze issues @@ -6,7 +43,6 @@ * add geolocation for android - ## 1.8.1 * Update to use the `verify` method introduced in platform_plugin_interface 2.1.0. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart index 728f8415a7b9..0e98ea08fd16 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'package:flutter/services.dart'; -import '../platform_interface/javascript_channel_registry.dart'; import '../platform_interface/platform_interface.dart'; import '../types/types.dart'; @@ -248,31 +247,29 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { static Map _webSettingsToMap(WebSettings? settings) { final Map map = {}; - void _addIfNonNull(String key, dynamic value) { + void addIfNonNull(String key, dynamic value) { if (value == null) { return; } map[key] = value; } - void _addSettingIfPresent(String key, WebSetting setting) { + void addSettingIfPresent(String key, WebSetting setting) { if (!setting.isPresent) { return; } map[key] = setting.value; } - _addIfNonNull('jsMode', settings!.javascriptMode?.index); - _addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate); - _addIfNonNull('hasProgressTracking', settings.hasProgressTracking); - _addIfNonNull('debuggingEnabled', settings.debuggingEnabled); - _addIfNonNull('geolocationEnabled', settings.geolocationEnabled); - _addIfNonNull( - 'gestureNavigationEnabled', settings.gestureNavigationEnabled); - _addIfNonNull( + addIfNonNull('jsMode', settings!.javascriptMode?.index); + addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate); + addIfNonNull('hasProgressTracking', settings.hasProgressTracking); + addIfNonNull('debuggingEnabled', settings.debuggingEnabled); + addIfNonNull('gestureNavigationEnabled', settings.gestureNavigationEnabled); + addIfNonNull( 'allowsInlineMediaPlayback', settings.allowsInlineMediaPlayback); - _addSettingIfPresent('userAgent', settings.userAgent); - _addIfNonNull('zoomEnabled', settings.zoomEnabled); + addSettingIfPresent('userAgent', settings.userAgent); + addIfNonNull('zoomEnabled', settings.zoomEnabled); return map; } diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart index fca8c5285711..e98ace5a0c8f 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart @@ -5,8 +5,7 @@ import 'package:flutter/widgets.dart'; import 'package:webview_pro_platform_interface/src/types/types.dart'; -import 'auto_media_playback_policy.dart'; -import 'web_settings.dart'; +import 'types.dart'; /// Configuration to use when creating a new [WebViewPlatformController]. /// diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart index fd47e18734f2..14a961cbc10a 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart @@ -62,7 +62,7 @@ class WebSetting { } @override - int get hashCode => hashValues(_value, isPresent); + int get hashCode => Object.hash(_value, isPresent); } /// Settings for configuring a WebViewPlatform. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_navigation_delegate.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_navigation_delegate.dart new file mode 100644 index 000000000000..a66f1defdf60 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_navigation_delegate.dart @@ -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. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'webview_platform.dart'; + +/// An interface defining navigation events that occur on the native platform. +/// +/// The [PlatformWebViewController] is notifying this delegate on events that +/// happened on the platform's webview. Platform implementations should +/// implement this class and pass an instance to the [PlatformWebViewController]. +abstract class PlatformNavigationDelegate extends PlatformInterface { + /// Creates a new [PlatformNavigationDelegate] + factory PlatformNavigationDelegate( + PlatformNavigationDelegateCreationParams params) { + final PlatformNavigationDelegate callbackDelegate = + WebViewPlatform.instance!.createPlatformNavigationDelegate(params); + PlatformInterface.verify(callbackDelegate, _token); + return callbackDelegate; + } + + /// Used by the platform implementation to create a new [PlatformNavigationDelegate]. + /// + /// Should only be used by platform implementations because they can't extend + /// a class that only contains a factory constructor. + @protected + PlatformNavigationDelegate.implementation(this.params) : super(token: _token); + + static final Object _token = Object(); + + /// The parameters used to initialize the [PlatformNavigationDelegate]. + final PlatformNavigationDelegateCreationParams params; + + /// Invoked when a navigation request is pending. + /// + /// See [PlatformWebViewController.setPlatformNavigationDelegate]. + Future setOnNavigationRequest( + FutureOr Function({required String url, required bool isForMainFrame}) + onNavigationRequest, + ) { + throw UnimplementedError( + 'setOnNavigationRequest is not implemented on the current platform.'); + } + + /// Invoked when a page has started loading. + /// + /// See [PlatformWebViewController.setPlatformNavigationDelegate]. + Future setOnPageStarted( + void Function(String url) onPageStarted, + ) { + throw UnimplementedError( + 'setOnPageStarted is not implemented on the current platform.'); + } + + /// Invoked when a page has finished loading. + /// + /// See [PlatformWebViewController.setPlatformNavigationDelegate]. + Future setOnPageFinished( + void Function(String url) onPageFinished, + ) { + throw UnimplementedError( + 'setOnPageFinished is not implemented on the current platform.'); + } + + /// Invoked when a page is loading to report the progress. + /// + /// See [PlatformWebViewController.setPlatformNavigationDelegate]. + Future setOnProgress( + void Function(int progress) onProgress, + ) { + throw UnimplementedError( + 'setOnProgress is not implemented on the current platform.'); + } + + /// Invoked when a resource loading error occurred. + /// + /// See [PlatformWebViewController.setPlatformNavigationDelegate]. + Future setOnWebResourceError( + void Function(WebResourceError error) onWebResourceError, + ) { + throw UnimplementedError( + 'setOnWebResourceError is not implemented on the current platform.'); + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_controller.dart new file mode 100644 index 000000000000..3585ec8b1886 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_controller.dart @@ -0,0 +1,285 @@ +// 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:math'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'platform_navigation_delegate.dart'; +import 'webview_platform.dart'; + +/// Interface for a platform implementation of a web view controller. +/// +/// Platform implementations should extend this class rather than implement it +/// as `webview_flutter` does not consider newly added methods to be breaking +/// changes. Extending this class (using `extends`) ensures that the subclass +/// will get the default implementation, while platform implementations that +/// `implements` this interface will be broken by newly added +/// [PlatformWebViewCookieManager] methods. +abstract class PlatformWebViewController extends PlatformInterface { + /// Creates a new [PlatformWebViewController] + factory PlatformWebViewController( + PlatformWebViewControllerCreationParams params) { + final PlatformWebViewController webViewControllerDelegate = + WebViewPlatform.instance!.createPlatformWebViewController(params); + PlatformInterface.verify(webViewControllerDelegate, _token); + return webViewControllerDelegate; + } + + /// Used by the platform implementation to create a new [PlatformWebViewController]. + /// + /// Should only be used by platform implementations because they can't extend + /// a class that only contains a factory constructor. + @protected + PlatformWebViewController.implementation(this.params) : super(token: _token); + + static final Object _token = Object(); + + /// The parameters used to initialize the [PlatformWebViewController]. + final PlatformWebViewControllerCreationParams params; + + /// Loads the file located on the specified [absoluteFilePath]. + /// + /// The [absoluteFilePath] parameter should contain the absolute path to the + /// file as it is stored on the device. For example: + /// `/Users/username/Documents/www/index.html`. + /// + /// Throws an ArgumentError if the [absoluteFilePath] does not exist. + Future loadFile( + String absoluteFilePath, + ) { + throw UnimplementedError( + 'loadFile is not implemented on the current platform'); + } + + /// Loads the Flutter asset specified in the pubspec.yaml file. + /// + /// Throws an ArgumentError if [key] is not part of the specified assets + /// in the pubspec.yaml file. + Future loadFlutterAsset( + String key, + ) { + throw UnimplementedError( + 'loadFlutterAsset is not implemented on the current platform'); + } + + /// Loads the supplied HTML string. + /// + /// The [baseUrl] parameter is used when resolving relative URLs within the + /// HTML string. + Future loadHtmlString( + String html, { + String? baseUrl, + }) { + throw UnimplementedError( + 'loadHtmlString is not implemented on the current platform'); + } + + /// Makes a specific HTTP request ands loads the response in the webview. + /// + /// [WebViewRequest.method] must be one of the supported HTTP methods + /// in [WebViewRequestMethod]. + /// + /// If [WebViewRequest.headers] is not empty, its key-value pairs will be + /// added as the headers for the request. + /// + /// If [WebViewRequest.body] is not null, it will be added as the body + /// for the request. + /// + /// Throws an ArgumentError if [WebViewRequest.uri] has empty scheme. + Future loadRequest( + LoadRequestParams params, + ) { + throw UnimplementedError( + 'loadRequest is not implemented on the current platform'); + } + + /// Accessor to the current URL that the WebView is displaying. + /// + /// If no URL was ever loaded, returns `null`. + Future currentUrl() { + throw UnimplementedError( + 'currentUrl is not implemented on the current platform'); + } + + /// Checks whether there's a back history item. + Future canGoBack() { + throw UnimplementedError( + 'canGoBack is not implemented on the current platform'); + } + + /// Checks whether there's a forward history item. + Future canGoForward() { + throw UnimplementedError( + 'canGoForward is not implemented on the current platform'); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + throw UnimplementedError( + 'goBack is not implemented on the current platform'); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + throw UnimplementedError( + 'goForward is not implemented on the current platform'); + } + + /// Reloads the current URL. + Future reload() { + throw UnimplementedError( + 'reload is not implemented on the current platform'); + } + + /// Clears all caches used by the [WebView]. + /// + /// The following caches are cleared: + /// 1. Browser HTTP Cache. + /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + Future clearCache() { + throw UnimplementedError( + 'clearCache is not implemented on the current platform'); + } + + /// Clears the local storage used by the [WebView]. + Future clearLocalStorage() { + throw UnimplementedError( + 'clearLocalStorage is not implemented on the current platform'); + } + + /// Sets the [PlatformNavigationDelegate] containing the callback methods that + /// are called during navigation events. + Future setPlatformNavigationDelegate( + PlatformNavigationDelegate handler) { + throw UnimplementedError( + 'setPlatformNavigationDelegate is not implemented on the current platform'); + } + + /// Runs the given JavaScript in the context of the current page. + /// + /// The Future completes with an error if a JavaScript error occurred. + Future runJavaScript(String javaScript) { + throw UnimplementedError( + 'runJavaScript is not implemented on the current platform'); + } + + /// Runs the given JavaScript in the context of the current page, and returns the result. + /// + /// The Future completes with an error if a JavaScript error occurred, or if the + /// type the given expression evaluates to is unsupported. Unsupported values include + /// certain non-primitive types on iOS, as well as `undefined` or `null` on iOS 14+. + Future runJavaScriptReturningResult(String javaScript) { + throw UnimplementedError( + 'runJavaScriptReturningResult is not implemented on the current platform'); + } + + /// Adds a new JavaScript channel to the set of enabled channels. + Future addJavaScriptChannel( + JavaScriptChannelParams javaScriptChannelParams, + ) { + throw UnimplementedError( + 'addJavaScriptChannel is not implemented on the current platform'); + } + + /// Removes the JavaScript channel with the matching name from the set of + /// enabled channels. + /// + /// This disables the channel with the matching name if it was previously + /// enabled through the [addJavaScriptChannel]. + Future removeJavaScriptChannel(String javaScriptChannelName) { + throw UnimplementedError( + 'removeJavaScriptChannel is not implemented on the current platform'); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + throw UnimplementedError( + 'getTitle is not implemented on the current platform'); + } + + /// Set the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the position to scroll to in WebView pixels. + Future scrollTo(int x, int y) { + throw UnimplementedError( + 'scrollTo is not implemented on the current platform'); + } + + /// Move the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by. + Future scrollBy(int x, int y) { + throw UnimplementedError( + 'scrollBy is not implemented on the current platform'); + } + + /// Return the current scroll position of this view. + /// + /// Scroll position is measured from the top left. + Future> getScrollPosition() { + throw UnimplementedError( + 'getScrollPosition is not implemented on the current platform'); + } + + /// Whether to enable the platform's webview content debugging tools. + Future enableDebugging(bool enabled) { + throw UnimplementedError( + 'enableDebugging is not implemented on the current platform'); + } + + /// Whether to allow swipe based navigation on supported platforms. + Future enableGestureNavigation(bool enabled) { + throw UnimplementedError( + 'enableGestureNavigation is not implemented on the current platform'); + } + + /// Whhether to support zooming using its on-screen zoom controls and gestures. + Future enableZoom(bool enabled) { + throw UnimplementedError( + 'enableZoom is not implemented on the current platform'); + } + + /// Set the current background color of this view. + Future setBackgroundColor(Color color) { + throw UnimplementedError( + 'setBackgroundColor is not implemented on the current platform'); + } + + /// Sets the JavaScript execution mode to be used by the webview. + Future setJavaScriptMode(JavaScriptMode javaScriptMode) { + throw UnimplementedError( + 'setJavaScriptMode is not implemented on the current platform'); + } + + /// Sets the value used for the HTTP `User-Agent:` request header. + Future setUserAgent(String? userAgent) { + throw UnimplementedError( + 'setUserAgent is not implemented on the current platform'); + } +} + +/// Describes the parameters necessary for registering a JavaScript channel. +class JavaScriptChannelParams { + /// Creates a new [JavaScriptChannelParams] object. + JavaScriptChannelParams({ + required this.name, + required this.onMessageReceived, + }); + + /// The name that identifies the JavaScript channel. + final String name; + + /// The callback method that is invoked when a [JavaScriptMessage] is + /// received. + final void Function(JavaScriptMessage) onMessageReceived; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_cookie_manager.dart new file mode 100644 index 000000000000..9e981c9022c6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_cookie_manager.dart @@ -0,0 +1,55 @@ +// 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:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'webview_platform.dart'; + +/// Interface for a platform implementation of a cookie manager. +/// +/// Platform implementations should extend this class rather than implement it +/// as `webview_flutter` does not consider newly added methods to be breaking +/// changes. Extending this class (using `extends`) ensures that the subclass +/// will get the default implementation, while platform implementations that +/// `implements` this interface will be broken by newly added +/// [PlatformWebViewCookieManager] methods. +abstract class PlatformWebViewCookieManager extends PlatformInterface { + /// Creates a new [PlatformWebViewCookieManager] + factory PlatformWebViewCookieManager( + PlatformWebViewCookieManagerCreationParams params) { + final PlatformWebViewCookieManager cookieManagerDelegate = + WebViewPlatform.instance!.createPlatformCookieManager(params); + PlatformInterface.verify(cookieManagerDelegate, _token); + return cookieManagerDelegate; + } + + /// Used by the platform implementation to create a new + /// [PlatformWebViewCookieManager]. + /// + /// Should only be used by platform implementations because they can't extend + /// a class that only contains a factory constructor. + @protected + PlatformWebViewCookieManager.implementation(this.params) + : super(token: _token); + + static final Object _token = Object(); + + /// The parameters used to initialize the [PlatformWebViewCookieManager]. + final PlatformWebViewCookieManagerCreationParams params; + + /// Clears all cookies for all [WebView] instances. + /// + /// Returns true if cookies were present before clearing, else false. + Future clearCookies() { + throw UnimplementedError( + 'clearCookies is not implemented on the current platform'); + } + + /// Sets a cookie for all [WebView] instances. + Future setCookie(WebViewCookie cookie) { + throw UnimplementedError( + 'setCookie is not implemented on the current platform'); + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_widget.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_widget.dart new file mode 100644 index 000000000000..40334c650b3a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_widget.dart @@ -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 'package:flutter/widgets.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'webview_platform.dart'; + +/// Interface for a platform implementation of a web view widget. +abstract class PlatformWebViewWidget extends PlatformInterface { + /// Creates a new [PlatformWebViewWidget] + factory PlatformWebViewWidget(PlatformWebViewWidgetCreationParams params) { + final PlatformWebViewWidget webViewWidgetDelegate = + WebViewPlatform.instance!.createPlatformWebViewWidget(params); + PlatformInterface.verify(webViewWidgetDelegate, _token); + return webViewWidgetDelegate; + } + + /// Used by the platform implementation to create a new + /// [PlatformWebViewWidget]. + /// + /// Should only be used by platform implementations because they can't extend + /// a class that only contains a factory constructor. + @protected + PlatformWebViewWidget.implementation(this.params) : super(token: _token); + + static final Object _token = Object(); + + /// The parameters used to initialize the [PlatformWebViewWidget]. + final PlatformWebViewWidgetCreationParams params; + + /// Builds a new WebView. + /// + /// Returns a Widget tree that embeds the created web view. + Widget build(BuildContext context); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/javascript_message.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/javascript_message.dart new file mode 100644 index 000000000000..b37661a045a9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/javascript_message.dart @@ -0,0 +1,51 @@ +// 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'; + +/// A message that was sent by JavaScript code running in a [WebView]. +/// +/// Platform specific implementations can add additional fields by extending +/// this class and providing a factory method that takes the +/// [JavaScriptMessage] as a parameter. +/// +/// {@tool sample} +/// This example demonstrates how to extend the [JavaScriptMessage] to +/// provide additional platform specific parameters. +/// +/// When extending [JavaScriptMessage] additional parameters should always +/// accept `null` or have a default value to prevent breaking changes. +/// +/// ```dart +/// @immutable +/// class WKWebViewScriptMessage extends JavaScriptMessage { +/// WKWebViewScriptMessage._( +/// JavaScriptMessage javaScriptMessage, +/// this.extraData, +/// ) : super(javaScriptMessage.message); +/// +/// factory WKWebViewScriptMessage.fromJavaScripMessage( +/// JavaScriptMessage javaScripMessage, { +/// String? extraData, +/// }) { +/// return WKWebViewScriptMessage._( +/// javaScriptMessage, +/// extraData: extraData, +/// ); +/// } +/// +/// final String? extraData; +/// } +/// ``` +/// {@end-tool} +@immutable +class JavaScriptMessage { + /// Creates a new JavaScript message object. + const JavaScriptMessage({ + required this.message, + }); + + /// The contents of the message that was sent by the JavaScript code. + final String message; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/javascript_mode.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/javascript_mode.dart new file mode 100644 index 000000000000..bcbebff8bb1a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/javascript_mode.dart @@ -0,0 +1,12 @@ +// 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. + +/// Describes the state of JavaScript support in a given web view. +enum JavaScriptMode { + /// JavaScript execution is disabled. + disabled, + + /// JavaScript execution is not restricted. + unrestricted, +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/load_request_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/load_request_params.dart new file mode 100644 index 000000000000..a0d1c8821798 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/load_request_params.dart @@ -0,0 +1,91 @@ +// 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:flutter/foundation.dart'; + +import '../platform_webview_controller.dart'; + +/// Defines the supported HTTP methods for loading a page in [PlatformWebViewController]. +enum LoadRequestMethod { + /// HTTP GET method. + get, + + /// HTTP POST method. + post, +} + +/// Extension methods on the [LoadRequestMethod] enum. +extension LoadRequestMethodExtensions on LoadRequestMethod { + /// Converts [LoadRequestMethod] to [String] format. + String serialize() { + switch (this) { + case LoadRequestMethod.get: + return 'get'; + case LoadRequestMethod.post: + return 'post'; + } + } +} + +/// Defines the parameters that can be used to load a page with the [PlatformWebViewController]. +/// +/// Platform specific implementations can add additional fields by extending +/// this class. +/// +/// {@tool sample} +/// This example demonstrates how to extend the [LoadRequestParams] to +/// provide additional platform specific parameters. +/// +/// When extending [LoadRequestParams] additional parameters should always +/// accept `null` or have a default value to prevent breaking changes. +/// +/// ```dart +/// class AndroidLoadRequestParams extends LoadRequestParams { +/// AndroidLoadRequestParams._({ +/// required LoadRequestParams params, +/// this.historyUrl, +/// }) : super( +/// uri: params.uri, +/// method: params.method, +/// body: params.body, +/// headers: params.headers, +/// ); +/// +/// factory AndroidLoadRequestParams.fromLoadRequestParams( +/// LoadRequestParams params, { +/// Uri? historyUrl, +/// }) { +/// return AndroidLoadRequestParams._(params, historyUrl: historyUrl); +/// } +/// +/// final Uri? historyUrl; +/// } +/// ``` +/// {@end-tool} +@immutable +class LoadRequestParams { + /// Used by the platform implementation to create a new [LoadRequestParams]. + const LoadRequestParams({ + required this.uri, + required this.method, + required this.headers, + this.body, + }); + + /// URI for the request. + final Uri uri; + + /// HTTP method used to make the request. + final LoadRequestMethod method; + + /// Headers for the request. + final Map headers; + + /// HTTP body for the request. + final Uint8List? body; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_navigation_delegate_creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_navigation_delegate_creation_params.dart new file mode 100644 index 000000000000..b20e5eb3ed48 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_navigation_delegate_creation_params.dart @@ -0,0 +1,44 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Object specifying creation parameters for creating a [PlatformNavigationDelegate]. +/// +/// Platform specific implementations can add additional fields by extending +/// this class. +/// +/// {@tool sample} +/// This example demonstrates how to extend the [PlatformNavigationDelegateCreationParams] to +/// provide additional platform specific parameters. +/// +/// When extending [PlatformNavigationDelegateCreationParams] additional +/// parameters should always accept `null` or have a default value to prevent +/// breaking changes. +/// +/// ```dart +/// class AndroidNavigationDelegateCreationParams extends PlatformNavigationDelegateCreationParams { +/// AndroidNavigationDelegateCreationParams._( +/// // This parameter prevents breaking changes later. +/// // ignore: avoid_unused_constructor_parameters +/// PlatformNavigationDelegateCreationParams params, { +/// this.filter, +/// }) : super(); +/// +/// factory AndroidNavigationDelegateCreationParams.fromPlatformNavigationDelegateCreationParams( +/// PlatformNavigationDelegateCreationParams params, { +/// String? filter, +/// }) { +/// return AndroidNavigationDelegateCreationParams._(params, filter: filter); +/// } +/// +/// final String? filter; +/// } +/// ``` +/// {@end-tool} +@immutable +class PlatformNavigationDelegateCreationParams { + /// Used by the platform implementation to create a new [PlatformNavigationkDelegate]. + const PlatformNavigationDelegateCreationParams(); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_controller_creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_controller_creation_params.dart new file mode 100644 index 000000000000..778396a79845 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_controller_creation_params.dart @@ -0,0 +1,45 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Object specifying creation parameters for creating a [PlatformWebViewController]. +/// +/// Platform specific implementations can add additional fields by extending +/// this class. +/// +/// {@tool sample} +/// This example demonstrates how to extend the [PlatformWebViewControllerCreationParams] to +/// provide additional platform specific parameters. +/// +/// When extending [PlatformWebViewControllerCreationParams] additional parameters +/// should always accept `null` or have a default value to prevent breaking +/// changes. +/// +/// ```dart +/// class WKWebViewControllerCreationParams +/// extends PlatformWebViewControllerCreationParams { +/// WKWebViewControllerCreationParams._( +/// // This parameter prevents breaking changes later. +/// // ignore: avoid_unused_constructor_parameters +/// PlatformWebViewControllerCreationParams params, { +/// this.domain, +/// }) : super(); +/// +/// factory WKWebViewControllerCreationParams.fromPlatformWebViewControllerCreationParams( +/// PlatformWebViewControllerCreationParams params, { +/// String? domain, +/// }) { +/// return WKWebViewControllerCreationParams._(params, domain: domain); +/// } +/// +/// final String? domain; +/// } +/// ``` +/// {@end-tool} +@immutable +class PlatformWebViewControllerCreationParams { + /// Used by the platform implementation to create a new [PlatformWebViewController]. + const PlatformWebViewControllerCreationParams(); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_cookie_manager_creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_cookie_manager_creation_params.dart new file mode 100644 index 000000000000..e8c4938f649f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_cookie_manager_creation_params.dart @@ -0,0 +1,45 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Object specifying creation parameters for creating a [PlatformWebViewCookieManager]. +/// +/// Platform specific implementations can add additional fields by extending +/// this class. +/// +/// {@tool sample} +/// This example demonstrates how to extend the [PlatformWebViewCookieManagerCreationParams] to +/// provide additional platform specific parameters. +/// +/// When extending [PlatformWebViewCookieManagerCreationParams] additional +/// parameters should always accept `null` or have a default value to prevent +/// breaking changes. +/// +/// ```dart +/// class WKWebViewCookieManagerCreationParams +/// extends PlatformWebViewCookieManagerCreationParams { +/// WKWebViewCookieManagerCreationParams._( +/// // This parameter prevents breaking changes later. +/// // ignore: avoid_unused_constructor_parameters +/// PlatformWebViewCookieManagerCreationParams params, { +/// this.uri, +/// }) : super(); +/// +/// factory WKWebViewCookieManagerCreationParams.fromPlatformWebViewCookieManagerCreationParams( +/// PlatformWebViewCookieManagerCreationParams params, { +/// Uri? uri, +/// }) { +/// return WKWebViewCookieManagerCreationParams._(params, uri: uri); +/// } +/// +/// final Uri? uri; +/// } +/// ``` +/// {@end-tool} +@immutable +class PlatformWebViewCookieManagerCreationParams { + /// Used by the platform implementation to create a new [PlatformWebViewCookieManagerDelegate]. + const PlatformWebViewCookieManagerCreationParams(); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_widget_creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_widget_creation_params.dart new file mode 100644 index 000000000000..83a73c2a44a3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_widget_creation_params.dart @@ -0,0 +1,84 @@ +// 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/gestures.dart'; +import 'package:flutter/painting.dart'; + +import '../platform_webview_controller.dart'; + +/// Object specifying creation parameters for creating a [WebViewWidgetDelegate]. +/// +/// Platform specific implementations can add additional fields by extending +/// this class. +/// +/// {@tool sample} +/// This example demonstrates how to extend the [PlatformWebViewWidgetCreationParams] to +/// provide additional platform specific parameters. +/// +/// When extending [PlatformWebViewWidgetCreationParams] additional parameters +/// should always accept `null` or have a default value to prevent breaking +/// changes. +/// +/// ```dart +/// class AndroidWebViewWidgetCreationParams +/// extends PlatformWebViewWidgetCreationParams { +/// AndroidWebViewWidgetCreationParams({ +/// super.key, +/// super.layoutDirection, +/// super.gestureRecognizers, +/// this.platformSpecificFieldExample, +/// }); +/// +/// WKWebViewWidgetCreationParams.fromPlatformWebViewWidgetCreationParams( +/// PlatformWebViewWidgetCreationParams params, { +/// Object? platformSpecificFieldExample, +/// }) : this( +/// key: params.key, +/// layoutDirection: params.layoutDirection, +/// gestureRecognizers: params.gestureRecognizers, +/// platformSpecificFieldExample: platformSpecificFieldExample, +/// ); +/// +/// final Object? platformSpecificFieldExample; +/// } +/// ``` +/// {@end-tool} +@immutable +class PlatformWebViewWidgetCreationParams { + /// Used by the platform implementation to create a new [PlatformWebViewWidget]. + const PlatformWebViewWidgetCreationParams({ + this.key, + required this.controller, + this.layoutDirection = TextDirection.ltr, + this.gestureRecognizers = const >{}, + }); + + /// Controls how one widget replaces another widget in the tree. + /// + /// See also: + /// + /// * The discussions at [Key] and [GlobalKey]. + final Key? key; + + /// The [PlatformWebViewController] that allows controlling the native web + /// view. + final PlatformWebViewController controller; + + /// The layout direction to use for the embedded WebView. + final TextDirection layoutDirection; + + /// The `gestureRecognizers` specifies which gestures should be consumed by the + /// web view. + /// + /// It is possible for other gesture recognizers to be competing with the web + /// view on pointer events, e.g if the web view is inside a [ListView] the + /// [ListView] will want to handle vertical drags. The web view will claim + /// gestures that are recognized by any of the recognizers on this list. + /// + /// When `gestureRecognizers` is empty (default), the web view will only handle + /// pointer events for gestures that were not claimed by any other gesture + /// recognizer. + final Set> gestureRecognizers; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/types.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/types.dart new file mode 100644 index 000000000000..05504fffd211 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/types.dart @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'javascript_message.dart'; +export 'javascript_mode.dart'; +export 'load_request_params.dart'; +export 'platform_navigation_delegate_creation_params.dart'; +export 'platform_webview_controller_creation_params.dart'; +export 'platform_webview_cookie_manager_creation_params.dart'; +export 'platform_webview_widget_creation_params.dart'; +export 'web_resource_error.dart'; +export 'webview_cookie.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/web_resource_error.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/web_resource_error.dart new file mode 100644 index 000000000000..465799472912 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/web_resource_error.dart @@ -0,0 +1,119 @@ +// 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'; + +/// Possible error type categorizations used by [WebResourceError]. +enum WebResourceErrorType { + /// User authentication failed on server. + authentication, + + /// Malformed URL. + badUrl, + + /// Failed to connect to the server. + connect, + + /// Failed to perform SSL handshake. + failedSslHandshake, + + /// Generic file error. + file, + + /// File not found. + fileNotFound, + + /// Server or proxy hostname lookup failed. + hostLookup, + + /// Failed to read or write to the server. + io, + + /// User authentication failed on proxy. + proxyAuthentication, + + /// Too many redirects. + redirectLoop, + + /// Connection timed out. + timeout, + + /// Too many requests during this load. + tooManyRequests, + + /// Generic error. + unknown, + + /// Resource load was canceled by Safe Browsing. + unsafeResource, + + /// Unsupported authentication scheme (not basic or digest). + unsupportedAuthScheme, + + /// Unsupported URI scheme. + unsupportedScheme, + + /// The web content process was terminated. + webContentProcessTerminated, + + /// The web view was invalidated. + webViewInvalidated, + + /// A JavaScript exception occurred. + javaScriptExceptionOccurred, + + /// The result of JavaScript execution could not be returned. + javaScriptResultTypeIsUnsupported, +} + +/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. +/// +/// Platform specific implementations can add additional fields by extending +/// this class. +/// +/// {@tool sample} +/// This example demonstrates how to extend the [WebResourceError] to +/// provide additional platform specific parameters. +/// +/// When extending [WebResourceError] additional parameters should always +/// accept `null` or have a default value to prevent breaking changes. +/// +/// ```dart +/// class IOSWebResourceError extends WebResourceError { +/// IOSWebResourceError._(WebResourceError error, {required this.domain}) +/// : super( +/// errorCode: error.errorCode, +/// description: error.description, +/// errorType: error.errorType, +/// ); +/// +/// factory IOSWebResourceError.fromWebResourceError( +/// WebResourceError error, { +/// required String? domain, +/// }) { +/// return IOSWebResourceError._(error, domain: domain); +/// } +/// +/// final String? domain; +/// } +/// ``` +/// {@end-tool} +@immutable +class WebResourceError { + /// Used by the platform implementation to create a new [WebResourceError]. + const WebResourceError({ + required this.errorCode, + required this.description, + this.errorType, + }); + + /// Raw code of the error from the respective platform. + final int errorCode; + + /// Description of the error that can be used to communicate the problem to the user. + final String description; + + /// The type this error can be categorized as. + final WebResourceErrorType? errorType; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/webview_cookie.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/webview_cookie.dart new file mode 100644 index 000000000000..7f56a312049f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/webview_cookie.dart @@ -0,0 +1,41 @@ +// 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'; + +/// A cookie that can be set globally for all web views using [WebViewCookieManagerPlatform]. +@immutable +class WebViewCookie { + /// Creates a new [WebViewCookieDelegate] + const WebViewCookie({ + required this.name, + required this.value, + required this.domain, + this.path = '/', + }); + + /// The cookie-name of the cookie. + /// + /// Its value should match "cookie-name" in RFC6265bis: + /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 + final String name; + + /// The cookie-value of the cookie. + /// + /// Its value should match "cookie-value" in RFC6265bis: + /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 + final String value; + + /// The domain-value of the cookie. + /// + /// Its value should match "domain-value" in RFC6265bis: + /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 + final String domain; + + /// The path-value of the cookie, set to `/` by default. + /// + /// Its value should match "path-value" in RFC6265bis: + /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 + final String path; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/webview_platform.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/webview_platform.dart new file mode 100644 index 000000000000..c5c5dffc6a22 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/webview_platform.dart @@ -0,0 +1,82 @@ +// 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:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'platform_navigation_delegate.dart'; +import 'platform_webview_controller.dart'; +import 'platform_webview_cookie_manager.dart'; +import 'platform_webview_widget.dart'; +import 'types/types.dart'; + +export 'types/types.dart'; + +/// Interface for a platform implementation of a WebView. +abstract class WebViewPlatform extends PlatformInterface { + /// Creates a new [WebViewPlatform]. + WebViewPlatform() : super(token: _token); + + static final Object _token = Object(); + + static WebViewPlatform? _instance; + + /// The instance of [WebViewPlatform] to use. + static WebViewPlatform? get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [WebViewPlatform] when they register themselves. + static set instance(WebViewPlatform? instance) { + if (instance == null) { + throw AssertionError( + 'Platform interfaces can only be set to a non-null instance'); + } + + PlatformInterface.verify(instance, _token); + _instance = instance; + } + + /// Creates a new [PlatformWebViewCookieManager]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebViewCookieManager] in `webview_flutter` instead. + PlatformWebViewCookieManager createPlatformCookieManager( + PlatformWebViewCookieManagerCreationParams params, + ) { + throw UnimplementedError( + 'createPlatformCookieManager is not implemented on the current platform.'); + } + + /// Creates a new [PlatformNavigationDelegate]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [NavigationDelegate] in `webview_flutter` instead. + PlatformNavigationDelegate createPlatformNavigationDelegate( + PlatformNavigationDelegateCreationParams params, + ) { + throw UnimplementedError( + 'createPlatformNavigationDelegate is not implemented on the current platform.'); + } + + /// Create a new [PlatformWebViewController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebViewController] in `webview_flutter` instead. + PlatformWebViewController createPlatformWebViewController( + PlatformWebViewControllerCreationParams params, + ) { + throw UnimplementedError( + 'createPlatformWebViewController is not implemented on the current platform.'); + } + + /// Create a new [PlatformWebViewWidget]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebViewWidget] in `webview_flutter` instead. + PlatformWebViewWidget createPlatformWebViewWidget( + PlatformWebViewWidgetCreationParams params, + ) { + throw UnimplementedError( + 'createPlatformWebViewWidget is not implemented on the current platform.'); + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/webview_flutter_platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/webview_flutter_platform_interface.dart new file mode 100644 index 000000000000..d14fec163327 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/webview_flutter_platform_interface.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. + +export 'src/platform_navigation_delegate.dart'; +export 'src/platform_webview_controller.dart'; +export 'src/platform_webview_cookie_manager.dart'; +export 'src/platform_webview_widget.dart'; +export 'src/types/types.dart'; +export 'src/webview_platform.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index 2a9095423bf1..e03bdfa80b84 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -4,19 +4,20 @@ repository: https://github.com/wenzhiming/flutter-plugins/tree/main/packages/web issue_tracker: https://github.com/wenzhiming/flutter-plugins/issues # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.8.1+2 +version: 1.9.5+1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" dependencies: flutter: sdk: flutter + meta: ^1.7.0 plugin_platform_interface: ^2.1.0 dev_dependencies: + build_runner: ^2.1.8 flutter_test: sdk: flutter mockito: ^5.0.0 - pedantic: ^1.10.0 diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart index 05902475e8ff..f8b5e84dce84 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart @@ -2,8 +2,12 @@ // 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#106316) +// ignore: unnecessary_import import 'dart:typed_data'; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -36,14 +40,11 @@ void main() { case 'loadFile': if (methodCall.arguments == 'invalid file') { throw PlatformException( - code: 'loadFile_failed', - message: 'Failed loading file.', - details: null); + code: 'loadFile_failed', message: 'Failed loading file.'); } else if (methodCall.arguments == 'some error') { throw PlatformException( code: 'some_error', message: 'Some error occurred.', - details: null, ); } return null; @@ -52,13 +53,11 @@ void main() { throw PlatformException( code: 'loadFlutterAsset_invalidKey', message: 'Failed loading asset.', - details: null, ); } else if (methodCall.arguments == 'some error') { throw PlatformException( code: 'some_error', message: 'Some error occurred.', - details: null, ); } return null; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart index f003ef430815..a323fc35b9dd 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart @@ -8,35 +8,35 @@ import 'package:webview_pro_platform_interface/src/types/javascript_channel.dart import 'package:webview_pro_platform_interface/src/types/types.dart'; void main() { - final Map _log = {}; - final Set _channels = { + final Map log = {}; + final Set channels = { JavascriptChannel( name: 'js_channel_1', onMessageReceived: (JavascriptMessage message) => - _log['js_channel_1'] = message.message, + log['js_channel_1'] = message.message, ), JavascriptChannel( name: 'js_channel_2', onMessageReceived: (JavascriptMessage message) => - _log['js_channel_2'] = message.message, + log['js_channel_2'] = message.message, ), JavascriptChannel( name: 'js_channel_3', onMessageReceived: (JavascriptMessage message) => - _log['js_channel_3'] = message.message, + log['js_channel_3'] = message.message, ), }; tearDown(() { - _log.clear(); + log.clear(); }); test('ctor should initialize with channels.', () { final JavascriptChannelRegistry registry = - JavascriptChannelRegistry(_channels); + JavascriptChannelRegistry(channels); expect(registry.channels.length, 3); - for (final JavascriptChannel channel in _channels) { + for (final JavascriptChannel channel in channels) { expect(registry.channels[channel.name], channel); } }); @@ -44,7 +44,7 @@ void main() { test('onJavascriptChannelMessage should forward message on correct channel.', () { final JavascriptChannelRegistry registry = - JavascriptChannelRegistry(_channels); + JavascriptChannelRegistry(channels); registry.onJavascriptChannelMessage( 'js_channel_2', @@ -52,7 +52,7 @@ void main() { ); expect( - _log, + log, containsPair( 'js_channel_2', 'test message on channel 2', @@ -63,7 +63,7 @@ void main() { 'onJavascriptChannelMessage should throw ArgumentError when message arrives on non-existing channel.', () { final JavascriptChannelRegistry registry = - JavascriptChannelRegistry(_channels); + JavascriptChannelRegistry(channels); expect( () => registry.onJavascriptChannelMessage( @@ -80,7 +80,7 @@ void main() { 'updateJavascriptChannelsFromSet should clear all channels when null is supplied.', () { final JavascriptChannelRegistry registry = - JavascriptChannelRegistry(_channels); + JavascriptChannelRegistry(channels); expect(registry.channels.length, 3); @@ -92,7 +92,7 @@ void main() { test('updateJavascriptChannelsFromSet should update registry with new set.', () { final JavascriptChannelRegistry registry = - JavascriptChannelRegistry(_channels); + JavascriptChannelRegistry(channels); expect(registry.channels.length, 3); @@ -100,12 +100,12 @@ void main() { JavascriptChannel( name: 'new_js_channel_1', onMessageReceived: (JavascriptMessage message) => - _log['new_js_channel_1'] = message.message, + log['new_js_channel_1'] = message.message, ), JavascriptChannel( name: 'new_js_channel_2', onMessageReceived: (JavascriptMessage message) => - _log['new_js_channel_2'] = message.message, + log['new_js_channel_2'] = message.message, ), }; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart index 5848d141190f..92a387cd9308 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart @@ -6,17 +6,17 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:webview_pro_platform_interface/src/types/javascript_channel.dart'; void main() { - final List _validChars = + final List validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_'.split(''); - final List _commonInvalidChars = + final List commonInvalidChars = r'`~!@#$%^&*()-=+[]{}\|"' ':;/?<>,. '.split(''); - final List _digits = List.generate(10, (int index) => index++); + final List digits = List.generate(10, (int index) => index++); test( 'ctor should create JavascriptChannel when name starts with a valid character followed by a number.', () { - for (final String char in _validChars) { - for (final int digit in _digits) { + for (final String char in validChars) { + for (final int digit in digits) { final JavascriptChannel channel = JavascriptChannel(name: '$char$digit', onMessageReceived: (_) {}); @@ -26,7 +26,7 @@ void main() { }); test('ctor should assert when channel name starts with a number.', () { - for (final int i in _digits) { + for (final int i in digits) { expect( () => JavascriptChannel(name: '$i', onMessageReceived: (_) {}), throwsAssertionError, @@ -35,8 +35,8 @@ void main() { }); test('ctor should assert when channel contains invalid char.', () { - for (final String validChar in _validChars) { - for (final String invalidChar in _commonInvalidChars) { + for (final String validChar in validChars) { + for (final String invalidChar in commonInvalidChars) { expect( () => JavascriptChannel( name: validChar + invalidChar, onMessageReceived: (_) {}), diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_navigation_delegate_test.dart new file mode 100644 index 000000000000..dd4a26c4faf9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_navigation_delegate_test.dart @@ -0,0 +1,148 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart'; +import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart'; + +import 'webview_platform_test.mocks.dart'; + +void main() { + setUp(() { + WebViewPlatform.instance = MockWebViewPlatformWithMixin(); + }); + + test('Cannot be implemented with `implements`', () { + const PlatformNavigationDelegateCreationParams params = + PlatformNavigationDelegateCreationParams(); + when(WebViewPlatform.instance!.createPlatformNavigationDelegate(params)) + .thenReturn(ImplementsPlatformNavigationDelegate()); + + expect(() { + PlatformNavigationDelegate(params); + // In versions of `package:plugin_platform_interface` prior to fixing + // https://github.com/flutter/flutter/issues/109339, an attempt to + // implement a platform interface using `implements` would sometimes throw + // a `NoSuchMethodError` and other times throw an `AssertionError`. After + // the issue is fixed, an `AssertionError` will always be thrown. For the + // purpose of this test, we don't really care what exception is thrown, so + // just allow any exception. + }, throwsA(anything)); + }); + + test('Can be extended', () { + const PlatformNavigationDelegateCreationParams params = + PlatformNavigationDelegateCreationParams(); + when(WebViewPlatform.instance!.createPlatformNavigationDelegate(params)) + .thenReturn(ExtendsPlatformNavigationDelegate(params)); + + expect(PlatformNavigationDelegate(params), isNotNull); + }); + + test('Can be mocked with `implements`', () { + const PlatformNavigationDelegateCreationParams params = + PlatformNavigationDelegateCreationParams(); + when(WebViewPlatform.instance!.createPlatformNavigationDelegate(params)) + .thenReturn(MockNavigationDelegate()); + + expect(PlatformNavigationDelegate(params), isNotNull); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of setOnNavigationRequest should throw unimplemented error', + () { + final PlatformNavigationDelegate callbackDelegate = + ExtendsPlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams()); + + expect( + () => callbackDelegate.setOnNavigationRequest( + ({required bool isForMainFrame, required String url}) => true), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of setOnPageStarted should throw unimplemented error', + () { + final PlatformNavigationDelegate callbackDelegate = + ExtendsPlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams()); + + expect( + () => callbackDelegate.setOnPageStarted((String url) {}), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of setOnPageFinished should throw unimplemented error', + () { + final PlatformNavigationDelegate callbackDelegate = + ExtendsPlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams()); + + expect( + () => callbackDelegate.setOnPageFinished((String url) {}), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of setOnProgress should throw unimplemented error', + () { + final PlatformNavigationDelegate callbackDelegate = + ExtendsPlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams()); + + expect( + () => callbackDelegate.setOnProgress((int progress) {}), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of setOnWebResourceError should throw unimplemented error', + () { + final PlatformNavigationDelegate callbackDelegate = + ExtendsPlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams()); + + expect( + () => callbackDelegate.setOnWebResourceError((WebResourceError error) {}), + throwsUnimplementedError, + ); + }); +} + +class MockWebViewPlatformWithMixin extends MockWebViewPlatform + with + // ignore: prefer_mixin + MockPlatformInterfaceMixin {} + +class ImplementsPlatformNavigationDelegate + implements PlatformNavigationDelegate { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class MockNavigationDelegate extends Mock + with + // ignore: prefer_mixin + MockPlatformInterfaceMixin + implements + PlatformNavigationDelegate {} + +class ExtendsPlatformNavigationDelegate extends PlatformNavigationDelegate { + ExtendsPlatformNavigationDelegate( + PlatformNavigationDelegateCreationParams params) + : super.implementation(params); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.dart new file mode 100644 index 000000000000..32374fb04484 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.dart @@ -0,0 +1,474 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart'; +import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart'; +import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart'; + +import 'platform_navigation_delegate_test.dart'; +import 'webview_platform_test.mocks.dart'; + +@GenerateMocks([PlatformNavigationDelegate]) +void main() { + setUp(() { + WebViewPlatform.instance = MockWebViewPlatformWithMixin(); + }); + + test('Cannot be implemented with `implements`', () { + when((WebViewPlatform.instance! as MockWebViewPlatform) + .createPlatformWebViewController(any)) + .thenReturn(ImplementsPlatformWebViewController()); + + expect(() { + PlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + // In versions of `package:plugin_platform_interface` prior to fixing + // https://github.com/flutter/flutter/issues/109339, an attempt to + // implement a platform interface using `implements` would sometimes throw + // a `NoSuchMethodError` and other times throw an `AssertionError`. After + // the issue is fixed, an `AssertionError` will always be thrown. For the + // purpose of this test, we don't really care what exception is thrown, so + // just allow any exception. + }, throwsA(anything)); + }); + + test('Can be extended', () { + const PlatformWebViewControllerCreationParams params = + PlatformWebViewControllerCreationParams(); + when((WebViewPlatform.instance! as MockWebViewPlatform) + .createPlatformWebViewController(any)) + .thenReturn(ExtendsPlatformWebViewController(params)); + + expect(PlatformWebViewController(params), isNotNull); + }); + + test('Can be mocked with `implements`', () { + when((WebViewPlatform.instance! as MockWebViewPlatform) + .createPlatformWebViewController(any)) + .thenReturn(MockWebViewControllerDelegate()); + + expect( + PlatformWebViewController( + const PlatformWebViewControllerCreationParams()), + isNotNull); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of loadFile should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.loadFile(''), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of loadFlutterAsset should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.loadFlutterAsset(''), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of loadHtmlString should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.loadHtmlString(''), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of loadRequest should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.loadRequest(MockLoadRequestParamsDelegate()), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of currentUrl should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.currentUrl(), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of canGoBack should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.canGoBack(), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of canGoForward should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.canGoForward(), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of goBack should throw unimplemented error', () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.goBack(), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of goForward should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.goForward(), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of reload should throw unimplemented error', () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.reload(), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of clearCache should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.clearCache(), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of clearLocalStorage should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.clearLocalStorage(), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of the setNavigationCallback should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => + controller.setPlatformNavigationDelegate(MockNavigationDelegate()), + throwsUnimplementedError, + ); + }, + ); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of runJavaScript should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.runJavaScript('javaScript'), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of runJavaScriptReturningResult should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.runJavaScriptReturningResult('javaScript'), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of addJavaScriptChannel should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.addJavaScriptChannel( + JavaScriptChannelParams( + name: 'test', + onMessageReceived: (_) {}, + ), + ), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of removeJavaScriptChannel should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.removeJavaScriptChannel('test'), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of getTitle should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.getTitle(), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of scrollTo should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.scrollTo(0, 0), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of scrollBy should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.scrollBy(0, 0), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of getScrollPosition should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.getScrollPosition(), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of enableDebugging should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.enableDebugging(true), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of enableGestureNavigation should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.enableGestureNavigation(true), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of enableZoom should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.enableZoom(true), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of setBackgroundColor should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.setBackgroundColor(Colors.blue), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of setJavaScriptMode should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.setJavaScriptMode(JavaScriptMode.disabled), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of setUserAgent should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.setUserAgent(null), + throwsUnimplementedError, + ); + }); +} + +class MockWebViewPlatformWithMixin extends MockWebViewPlatform + with + // ignore: prefer_mixin + MockPlatformInterfaceMixin {} + +class ImplementsPlatformWebViewController implements PlatformWebViewController { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class MockWebViewControllerDelegate extends Mock + with + // ignore: prefer_mixin + MockPlatformInterfaceMixin + implements + PlatformWebViewController {} + +class ExtendsPlatformWebViewController extends PlatformWebViewController { + ExtendsPlatformWebViewController( + PlatformWebViewControllerCreationParams params) + : super.implementation(params); +} + +// ignore: must_be_immutable +class MockLoadRequestParamsDelegate extends Mock + with + //ignore: prefer_mixin + MockPlatformInterfaceMixin + implements + LoadRequestParams {} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.mocks.dart new file mode 100644 index 000000000000..47e67379f124 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.mocks.dart @@ -0,0 +1,72 @@ +// Mocks generated by Mockito 5.0.16 from annotations +// in webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i4; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart' + as _i3; +import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart' + as _i2; + +// 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 + +class _FakePlatformNavigationDelegateCreationParams_0 extends _i1.Fake + implements _i2.PlatformNavigationDelegateCreationParams {} + +/// A class which mocks [PlatformNavigationDelegate]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlatformNavigationDelegate extends _i1.Mock + implements _i3.PlatformNavigationDelegate { + MockPlatformNavigationDelegate() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.PlatformNavigationDelegateCreationParams get params => + (super.noSuchMethod(Invocation.getter(#params), + returnValue: _FakePlatformNavigationDelegateCreationParams_0()) + as _i2.PlatformNavigationDelegateCreationParams); + @override + _i4.Future setOnNavigationRequest( + _i4.FutureOr Function({bool isForMainFrame, String url})? + onNavigationRequest) => + (super.noSuchMethod( + Invocation.method(#setOnNavigationRequest, [onNavigationRequest]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future setOnPageStarted(void Function(String)? onPageStarted) => + (super.noSuchMethod(Invocation.method(#setOnPageStarted, [onPageStarted]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future setOnPageFinished(void Function(String)? onPageFinished) => + (super.noSuchMethod( + Invocation.method(#setOnPageFinished, [onPageFinished]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future setOnProgress(void Function(int)? onProgress) => + (super.noSuchMethod(Invocation.method(#setOnProgress, [onProgress]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future setOnWebResourceError( + void Function(_i2.WebResourceError)? onWebResourceError) => + (super.noSuchMethod( + Invocation.method(#setOnWebResourceError, [onWebResourceError]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + String toString() => super.toString(); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_widget_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_widget_test.dart new file mode 100644 index 000000000000..ede16c162413 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_widget_test.dart @@ -0,0 +1,96 @@ +// 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/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart'; +import 'package:webview_flutter_platform_interface/v4/src/platform_webview_widget.dart'; +import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart'; + +import 'webview_platform_test.mocks.dart'; + +void main() { + setUp(() { + WebViewPlatform.instance = MockWebViewPlatformWithMixin(); + }); + + test('Cannot be implemented with `implements`', () { + final MockWebViewControllerDelegate controller = + MockWebViewControllerDelegate(); + final PlatformWebViewWidgetCreationParams params = + PlatformWebViewWidgetCreationParams(controller: controller); + when(WebViewPlatform.instance!.createPlatformWebViewWidget(params)) + .thenReturn(ImplementsWebViewWidgetDelegate()); + + expect(() { + PlatformWebViewWidget(params); + // In versions of `package:plugin_platform_interface` prior to fixing + // https://github.com/flutter/flutter/issues/109339, an attempt to + // implement a platform interface using `implements` would sometimes throw + // a `NoSuchMethodError` and other times throw an `AssertionError`. After + // the issue is fixed, an `AssertionError` will always be thrown. For the + // purpose of this test, we don't really care what exception is thrown, so + // just allow any exception. + }, throwsA(anything)); + }); + + test('Can be extended', () { + final MockWebViewControllerDelegate controller = + MockWebViewControllerDelegate(); + final PlatformWebViewWidgetCreationParams params = + PlatformWebViewWidgetCreationParams(controller: controller); + when(WebViewPlatform.instance!.createPlatformWebViewWidget(params)) + .thenReturn(ExtendsWebViewWidgetDelegate(params)); + + expect(PlatformWebViewWidget(params), isNotNull); + }); + + test('Can be mocked with `implements`', () { + final MockWebViewControllerDelegate controller = + MockWebViewControllerDelegate(); + final PlatformWebViewWidgetCreationParams params = + PlatformWebViewWidgetCreationParams(controller: controller); + when(WebViewPlatform.instance!.createPlatformWebViewWidget(params)) + .thenReturn(MockWebViewWidgetDelegate()); + + expect(PlatformWebViewWidget(params), isNotNull); + }); +} + +class MockWebViewPlatformWithMixin extends MockWebViewPlatform + with + // ignore: prefer_mixin + MockPlatformInterfaceMixin {} + +class ImplementsWebViewWidgetDelegate implements PlatformWebViewWidget { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class MockWebViewWidgetDelegate extends Mock + with + // ignore: prefer_mixin + MockPlatformInterfaceMixin + implements + PlatformWebViewWidget {} + +class ExtendsWebViewWidgetDelegate extends PlatformWebViewWidget { + ExtendsWebViewWidgetDelegate(PlatformWebViewWidgetCreationParams params) + : super.implementation(params); + + @override + Widget build(BuildContext context) { + throw UnimplementedError( + 'build is not implemented for ExtendedWebViewWidgetDelegate.'); + } +} + +class MockWebViewControllerDelegate extends Mock + with + // ignore: prefer_mixin + MockPlatformInterfaceMixin + implements + PlatformWebViewController {} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.dart new file mode 100644 index 000000000000..4ab6d587b879 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.dart @@ -0,0 +1,116 @@ +// 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:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart'; +import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart'; + +import 'webview_platform_test.mocks.dart'; + +@GenerateMocks([WebViewPlatform]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('Default instance WebViewPlatform instance should be null', () { + expect(WebViewPlatform.instance, isNull); + }); + + test('Cannot be implemented with `implements`', () { + expect(() { + WebViewPlatform.instance = ImplementsWebViewPlatform(); + // In versions of `package:plugin_platform_interface` prior to fixing + // https://github.com/flutter/flutter/issues/109339, an attempt to + // implement a platform interface using `implements` would sometimes throw + // a `NoSuchMethodError` and other times throw an `AssertionError`. After + // the issue is fixed, an `AssertionError` will always be thrown. For the + // purpose of this test, we don't really care what exception is thrown, so + // just allow any exception. + }, throwsA(anything)); + }); + + test('Can be extended', () { + WebViewPlatform.instance = ExtendsWebViewPlatform(); + }); + + test('Can be mocked with `implements`', () { + final MockWebViewPlatform mock = MockWebViewPlatformWithMixin(); + WebViewPlatform.instance = mock; + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of createCookieManagerDelegate should throw unimplemented error', + () { + final WebViewPlatform webViewPlatform = ExtendsWebViewPlatform(); + + expect( + () => webViewPlatform.createPlatformCookieManager( + const PlatformWebViewCookieManagerCreationParams()), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of createNavigationCallbackHandlerDelegate should throw unimplemented error', + () { + final WebViewPlatform webViewPlatform = ExtendsWebViewPlatform(); + + expect( + () => webViewPlatform.createPlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams()), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of createWebViewControllerDelegate should throw unimplemented error', + () { + final WebViewPlatform webViewPlatform = ExtendsWebViewPlatform(); + + expect( + () => webViewPlatform.createPlatformWebViewController( + const PlatformWebViewControllerCreationParams()), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of createWebViewWidgetDelegate should throw unimplemented error', + () { + final WebViewPlatform webViewPlatform = ExtendsWebViewPlatform(); + final MockWebViewControllerDelegate controller = + MockWebViewControllerDelegate(); + + expect( + () => webViewPlatform.createPlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller)), + throwsUnimplementedError, + ); + }); +} + +class ImplementsWebViewPlatform implements WebViewPlatform { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class MockWebViewPlatformWithMixin extends MockWebViewPlatform + with + // ignore: prefer_mixin + MockPlatformInterfaceMixin {} + +class ExtendsWebViewPlatform extends WebViewPlatform {} + +class MockWebViewControllerDelegate extends Mock + with + // ignore: prefer_mixin + MockPlatformInterfaceMixin + implements + PlatformWebViewController {} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.mocks.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.mocks.dart new file mode 100644 index 000000000000..5ce007579473 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.mocks.dart @@ -0,0 +1,78 @@ +// Mocks generated by Mockito 5.0.16 from annotations +// in webview_flutter_platform_interface/test/src/v4/webview_platform_test.dart. +// Do not manually edit this file. + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart' + as _i3; +import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart' + as _i4; +import 'package:webview_flutter_platform_interface/v4/src/platform_webview_cookie_manager.dart' + as _i2; +import 'package:webview_flutter_platform_interface/v4/src/platform_webview_widget.dart' + as _i5; +import 'package:webview_flutter_platform_interface/v4/src/types/types.dart' + as _i7; +import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart' + as _i6; + +// 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 + +class _FakePlatformWebViewCookieManager_0 extends _i1.Fake + implements _i2.PlatformWebViewCookieManager {} + +class _FakePlatformNavigationDelegate_1 extends _i1.Fake + implements _i3.PlatformNavigationDelegate {} + +class _FakePlatformWebViewController_2 extends _i1.Fake + implements _i4.PlatformWebViewController {} + +class _FakePlatformWebViewWidget_3 extends _i1.Fake + implements _i5.PlatformWebViewWidget {} + +/// A class which mocks [WebViewPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatform extends _i1.Mock implements _i6.WebViewPlatform { + MockWebViewPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.PlatformWebViewCookieManager createPlatformCookieManager( + _i7.PlatformWebViewCookieManagerCreationParams? params) => + (super.noSuchMethod( + Invocation.method(#createPlatformCookieManager, [params]), + returnValue: _FakePlatformWebViewCookieManager_0()) + as _i2.PlatformWebViewCookieManager); + @override + _i3.PlatformNavigationDelegate createPlatformNavigationDelegate( + _i7.PlatformNavigationDelegateCreationParams? params) => + (super.noSuchMethod( + Invocation.method(#createPlatformNavigationDelegate, [params]), + returnValue: _FakePlatformNavigationDelegate_1()) + as _i3.PlatformNavigationDelegate); + @override + _i4.PlatformWebViewController createPlatformWebViewController( + _i7.PlatformWebViewControllerCreationParams? params) => + (super.noSuchMethod( + Invocation.method(#createPlatformWebViewController, [params]), + returnValue: _FakePlatformWebViewController_2()) + as _i4.PlatformWebViewController); + @override + _i5.PlatformWebViewWidget createPlatformWebViewWidget( + _i7.PlatformWebViewWidgetCreationParams? params) => + (super.noSuchMethod( + Invocation.method(#createPlatformWebViewWidget, [params]), + returnValue: _FakePlatformWebViewWidget_3()) + as _i5.PlatformWebViewWidget); + @override + String toString() => super.toString(); +} diff --git a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md index ba8fc0fb01c9..82be36f7d094 100644 --- a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md @@ -1,6 +1,21 @@ ## NEXT +* Updates minimum Flutter version to 2.10. + +## 0.1.0+4 + +* Fixes incorrect escaping of some characters when setting the HTML to the iframe element. + +## 0.1.0+3 + +* Minor fixes for new analysis options. + +## 0.1.0+2 + +* Removes unnecessary imports. * Fixes unit tests to run on latest `master` version of Flutter. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. ## 0.1.0+1 diff --git a/packages/webview_flutter/webview_flutter_web/example/README.md b/packages/webview_flutter/webview_flutter_web/example/README.md index 850ee74397a9..96b8bb17dbff 100644 --- a/packages/webview_flutter/webview_flutter_web/example/README.md +++ b/packages/webview_flutter/webview_flutter_web/example/README.md @@ -1,8 +1,9 @@ -# webview_flutter_example +# Platform Implementation Test App -Demonstrates how to use the webview_flutter plugin. +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. -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/webview_flutter/webview_flutter_web/example/lib/main.dart b/packages/webview_flutter/webview_flutter_web/example/lib/main.dart index 9cf412d74e50..c183625be634 100644 --- a/packages/webview_flutter/webview_flutter_web/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_web/example/lib/main.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; diff --git a/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart index 787b016d2b77..ffd3367d33f4 100644 --- a/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart @@ -4,7 +4,6 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_web/webview_flutter_web.dart'; @@ -47,7 +46,7 @@ class WebView extends StatefulWidget { final String? initialUrl; @override - _WebViewState createState() => _WebViewState(); + State createState() => _WebViewState(); } class _WebViewState extends State { diff --git a/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml index a98df799e92c..e2e0796e7ea3 100644 --- a/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter + webview_flutter_platform_interface: ^1.8.0 webview_flutter_web: # When depending on this package from a real application you should use: # webview_flutter_web: ^x.y.z @@ -19,14 +20,12 @@ dependencies: path: ../ dev_dependencies: - espresso: ^0.1.0+2 flutter_driver: sdk: flutter flutter_test: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_fake.dart b/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_fake.dart index 8757ca22be17..40d8f1903111 100644 --- a/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_fake.dart +++ b/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_fake.dart @@ -11,10 +11,10 @@ import 'dart:html' as html; // ignore_for_file: camel_case_types /// Shim for web_ui engine.PlatformViewRegistry -/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62 +/// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/ui.dart#L62 class platformViewRegistry { /// Shim for registerViewFactory - /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72 + /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/ui.dart#L72 static bool registerViewFactory( String viewTypeId, html.Element Function(int viewId) viewFactory) { return false; @@ -22,10 +22,10 @@ class platformViewRegistry { } /// Shim for web_ui engine.AssetManager. -/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12 +/// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L12 class webOnlyAssetManager { /// Shim for getAssetUrl. - /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L45 + /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L45 static String getAssetUrl(String asset) => ''; } diff --git a/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart b/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart index aa427eb6874a..adf6495b8f2a 100644 --- a/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart +++ b/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:html'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -44,6 +45,7 @@ class WebWebViewPlatform implements WebViewPlatform { final IFrameElement element = document.getElementById('webview-$viewId')! as IFrameElement; if (creationParams.initialUrl != null) { + // ignore: unsafe_html element.src = creationParams.initialUrl; } onWebViewPlatformCreated(WebWebViewPlatformController( @@ -70,6 +72,7 @@ class WebWebViewPlatformController implements WebViewPlatformController { /// Setter for setting the HttpRequestFactory, for testing purposes. @visibleForTesting + // ignore: avoid_setters_without_getters set httpRequestFactory(HttpRequestFactory factory) { _httpRequestFactory = factory; } @@ -131,6 +134,7 @@ class WebWebViewPlatformController implements WebViewPlatformController { @override Future loadUrl(String url, Map? headers) async { + // ignore: unsafe_html _element.src = url; } @@ -179,7 +183,12 @@ class WebWebViewPlatformController implements WebViewPlatformController { String html, { String? baseUrl, }) async { - _element.src = 'data:text/html,' + Uri.encodeFull(html); + // ignore: unsafe_html + _element.src = Uri.dataFromString( + html, + mimeType: 'text/html', + encoding: utf8, + ).toString(); } @override @@ -194,8 +203,12 @@ class WebWebViewPlatformController implements WebViewPlatformController { sendData: request.body); final String contentType = httpReq.getResponseHeader('content-type') ?? 'text/html'; - _element.src = - 'data:$contentType,' + Uri.encodeFull(httpReq.responseText ?? ''); + // ignore: unsafe_html + _element.src = Uri.dataFromString( + httpReq.responseText ?? '', + mimeType: contentType, + encoding: utf8, + ).toString(); } @override @@ -265,7 +278,7 @@ class HttpRequestFactory { String? mimeType, Map? requestHeaders, dynamic sendData, - void onProgress(ProgressEvent e)?}) { + void Function(ProgressEvent e)? onProgress}) { return HttpRequest.request(url, method: method, withCredentials: withCredentials, diff --git a/packages/webview_flutter/webview_flutter_web/pubspec.yaml b/packages/webview_flutter/webview_flutter_web/pubspec.yaml index bd154387097e..f27e6408335a 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.1.0+1 +version: 0.1.0+4 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: @@ -29,4 +29,4 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - mockito: ^5.0.0 + mockito: ^5.3.2 diff --git a/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart index 90e2ea465782..08337e42e661 100644 --- a/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart +++ b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart @@ -54,17 +54,33 @@ void main() { verify(mockElement.src = 'test url'); }); - test('loadHtmlString loads html into iframe', () { - // Setup - final MockIFrameElement mockElement = MockIFrameElement(); - final WebWebViewPlatformController controller = - WebWebViewPlatformController( - mockElement, - ); - // Run - controller.loadHtmlString('test html'); - // Verify - verify(mockElement.src = 'data:text/html,' + Uri.encodeFull('test html')); + group('loadHtmlString', () { + test('loadHtmlString loads html into iframe', () { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + // Run + controller.loadHtmlString('test html'); + // Verify + verify(mockElement.src = + 'data:text/html;charset=utf-8,${Uri.encodeFull('test html')}'); + }); + + test('loadHtmlString escapes "#" correctly', () { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + // Run + controller.loadHtmlString('#'); + // Verify + verify(mockElement.src = argThat(contains('%23'))); + }); }); group('loadRequest', () { @@ -122,8 +138,40 @@ void main() { requestHeaders: {'Foo': 'Bar'}, sendData: Uint8List.fromList('test body'.codeUnits), )); - verify( - mockElement.src = 'data:text/plain,' + Uri.encodeFull('test data')); + verify(mockElement.src = + 'data:;charset=utf-8,${Uri.encodeFull('test data')}'); + }); + + test('loadRequest escapes "#" correctly', () async { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + final MockHttpRequest mockHttpRequest = MockHttpRequest(); + when(mockHttpRequest.getResponseHeader('content-type')) + .thenReturn('text/html'); + when(mockHttpRequest.responseText).thenReturn('#'); + final MockHttpRequestFactory mockHttpRequestFactory = + MockHttpRequestFactory(); + when(mockHttpRequestFactory.request( + any, + method: anyNamed('method'), + requestHeaders: anyNamed('requestHeaders'), + sendData: anyNamed('sendData'), + )).thenAnswer((_) => Future.value(mockHttpRequest)); + controller.httpRequestFactory = mockHttpRequestFactory; + // Run + await controller.loadRequest( + WebViewRequest( + uri: Uri.parse('https://flutter.dev'), + method: WebViewRequestMethod.post, + body: Uint8List.fromList('test body'.codeUnits), + headers: {'Foo': 'Bar'}), + ); + // Verify + verify(mockElement.src = argThat(contains('%23'))); }); }); }); diff --git a/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.mocks.dart b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.mocks.dart index e35d1e93c59f..db442eeea7a3 100644 --- a/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.mocks.dart @@ -1,25 +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. - -// Mocks generated by Mockito 5.0.16 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in webview_flutter_web/test/webview_flutter_web_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i6; import 'dart:html' as _i2; import 'dart:math' as _i3; import 'package:flutter/foundation.dart' as _i5; +import 'package:flutter/src/widgets/notification_listener.dart' as _i7; import 'package:flutter/widgets.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_platform_interface/src/types/auto_media_playback_policy.dart' - as _i8; -import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i7; +import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i8; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' as _i9; import 'package:webview_flutter_web/webview_flutter_web.dart' as _i10; +// ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references @@ -28,66 +25,231 @@ import 'package:webview_flutter_web/webview_flutter_web.dart' as _i10; // 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 _FakeCssClassSet_0 extends _i1.SmartFake implements _i2.CssClassSet { + _FakeCssClassSet_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeCssClassSet_0 extends _i1.Fake implements _i2.CssClassSet {} +class _FakeRectangle_1 extends _i1.SmartFake + implements _i3.Rectangle { + _FakeRectangle_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeRectangle_1 extends _i1.Fake - implements _i3.Rectangle {} +class _FakeCssRect_2 extends _i1.SmartFake implements _i2.CssRect { + _FakeCssRect_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeCssRect_2 extends _i1.Fake implements _i2.CssRect {} +class _FakePoint_3 extends _i1.SmartFake + implements _i3.Point { + _FakePoint_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakePoint_3 extends _i1.Fake implements _i3.Point {} +class _FakeElementEvents_4 extends _i1.SmartFake implements _i2.ElementEvents { + _FakeElementEvents_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeElementEvents_4 extends _i1.Fake implements _i2.ElementEvents {} +class _FakeCssStyleDeclaration_5 extends _i1.SmartFake + implements _i2.CssStyleDeclaration { + _FakeCssStyleDeclaration_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeCssStyleDeclaration_5 extends _i1.Fake - implements _i2.CssStyleDeclaration {} +class _FakeElementStream_6 extends _i1.SmartFake + implements _i2.ElementStream { + _FakeElementStream_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeElementStream_6 extends _i1.Fake - implements _i2.ElementStream {} +class _FakeElementList_7 extends _i1.SmartFake + implements _i2.ElementList { + _FakeElementList_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeElementList_7 extends _i1.Fake - implements _i2.ElementList {} +class _FakeScrollState_8 extends _i1.SmartFake implements _i2.ScrollState { + _FakeScrollState_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeScrollState_8 extends _i1.Fake implements _i2.ScrollState {} +class _FakeAnimation_9 extends _i1.SmartFake implements _i2.Animation { + _FakeAnimation_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeAnimation_9 extends _i1.Fake implements _i2.Animation {} +class _FakeElement_10 extends _i1.SmartFake implements _i2.Element { + _FakeElement_10( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeElement_10 extends _i1.Fake implements _i2.Element {} +class _FakeShadowRoot_11 extends _i1.SmartFake implements _i2.ShadowRoot { + _FakeShadowRoot_11( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeShadowRoot_11 extends _i1.Fake implements _i2.ShadowRoot {} +class _FakeDocumentFragment_12 extends _i1.SmartFake + implements _i2.DocumentFragment { + _FakeDocumentFragment_12( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeDocumentFragment_12 extends _i1.Fake - implements _i2.DocumentFragment {} +class _FakeNode_13 extends _i1.SmartFake implements _i2.Node { + _FakeNode_13( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeNode_13 extends _i1.Fake implements _i2.Node {} +class _FakeWidget_14 extends _i1.SmartFake implements _i4.Widget { + _FakeWidget_14( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); -class _FakeWidget_14 extends _i1.Fake implements _i4.Widget { @override String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => super.toString(); } -class _FakeInheritedWidget_15 extends _i1.Fake implements _i4.InheritedWidget { +class _FakeInheritedWidget_15 extends _i1.SmartFake + implements _i4.InheritedWidget { + _FakeInheritedWidget_15( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + @override String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => super.toString(); } -class _FakeDiagnosticsNode_16 extends _i1.Fake implements _i5.DiagnosticsNode { +class _FakeDiagnosticsNode_16 extends _i1.SmartFake + implements _i5.DiagnosticsNode { + _FakeDiagnosticsNode_16( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + @override - String toString( - {_i5.TextTreeConfiguration? parentConfiguration, - _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + String toString({ + _i5.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info, + }) => super.toString(); } -class _FakeHttpRequest_17 extends _i1.Fake implements _i2.HttpRequest {} +class _FakeHttpRequest_17 extends _i1.SmartFake implements _i2.HttpRequest { + _FakeHttpRequest_17( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeHttpRequestUpload_18 extends _i1.Fake - implements _i2.HttpRequestUpload {} +class _FakeHttpRequestUpload_18 extends _i1.SmartFake + implements _i2.HttpRequestUpload { + _FakeHttpRequestUpload_18( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeEvents_19 extends _i1.Fake implements _i2.Events {} +class _FakeEvents_19 extends _i1.SmartFake implements _i2.Events { + _FakeEvents_19( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} /// A class which mocks [IFrameElement]. /// @@ -98,907 +260,1844 @@ class MockIFrameElement extends _i1.Mock implements _i2.IFrameElement { } @override - set allow(String? value) => - super.noSuchMethod(Invocation.setter(#allow, value), - returnValueForMissingStub: null); - @override - set allowFullscreen(bool? value) => - super.noSuchMethod(Invocation.setter(#allowFullscreen, value), - returnValueForMissingStub: null); - @override - set allowPaymentRequest(bool? value) => - super.noSuchMethod(Invocation.setter(#allowPaymentRequest, value), - returnValueForMissingStub: null); - @override - set csp(String? value) => super.noSuchMethod(Invocation.setter(#csp, value), - returnValueForMissingStub: null); - @override - set height(String? value) => - super.noSuchMethod(Invocation.setter(#height, value), - returnValueForMissingStub: null); - @override - set name(String? value) => super.noSuchMethod(Invocation.setter(#name, value), - returnValueForMissingStub: null); - @override - set referrerPolicy(String? value) => - super.noSuchMethod(Invocation.setter(#referrerPolicy, value), - returnValueForMissingStub: null); - @override - set src(String? value) => super.noSuchMethod(Invocation.setter(#src, value), - returnValueForMissingStub: null); - @override - set srcdoc(String? value) => - super.noSuchMethod(Invocation.setter(#srcdoc, value), - returnValueForMissingStub: null); - @override - set width(String? value) => - super.noSuchMethod(Invocation.setter(#width, value), - returnValueForMissingStub: null); - @override - set nonce(String? value) => - super.noSuchMethod(Invocation.setter(#nonce, value), - returnValueForMissingStub: null); - @override - Map get attributes => - (super.noSuchMethod(Invocation.getter(#attributes), - returnValue: {}) as Map); - @override - set attributes(Map? value) => - super.noSuchMethod(Invocation.setter(#attributes, value), - returnValueForMissingStub: null); - @override - List<_i2.Element> get children => - (super.noSuchMethod(Invocation.getter(#children), - returnValue: <_i2.Element>[]) as List<_i2.Element>); - @override - set children(List<_i2.Element>? value) => - super.noSuchMethod(Invocation.setter(#children, value), - returnValueForMissingStub: null); - @override - _i2.CssClassSet get classes => - (super.noSuchMethod(Invocation.getter(#classes), - returnValue: _FakeCssClassSet_0()) as _i2.CssClassSet); - @override - set classes(Iterable? value) => - super.noSuchMethod(Invocation.setter(#classes, value), - returnValueForMissingStub: null); - @override - Map get dataset => - (super.noSuchMethod(Invocation.getter(#dataset), - returnValue: {}) as Map); - @override - set dataset(Map? value) => - super.noSuchMethod(Invocation.setter(#dataset, value), - returnValueForMissingStub: null); - @override - _i3.Rectangle get client => - (super.noSuchMethod(Invocation.getter(#client), - returnValue: _FakeRectangle_1()) as _i3.Rectangle); - @override - _i3.Rectangle get offset => - (super.noSuchMethod(Invocation.getter(#offset), - returnValue: _FakeRectangle_1()) as _i3.Rectangle); - @override - String get localName => - (super.noSuchMethod(Invocation.getter(#localName), returnValue: '') - as String); - @override - _i2.CssRect get contentEdge => - (super.noSuchMethod(Invocation.getter(#contentEdge), - returnValue: _FakeCssRect_2()) as _i2.CssRect); - @override - _i2.CssRect get paddingEdge => - (super.noSuchMethod(Invocation.getter(#paddingEdge), - returnValue: _FakeCssRect_2()) as _i2.CssRect); - @override - _i2.CssRect get borderEdge => - (super.noSuchMethod(Invocation.getter(#borderEdge), - returnValue: _FakeCssRect_2()) as _i2.CssRect); - @override - _i2.CssRect get marginEdge => - (super.noSuchMethod(Invocation.getter(#marginEdge), - returnValue: _FakeCssRect_2()) as _i2.CssRect); - @override - _i3.Point get documentOffset => - (super.noSuchMethod(Invocation.getter(#documentOffset), - returnValue: _FakePoint_3()) as _i3.Point); - @override - set innerHtml(String? html) => - super.noSuchMethod(Invocation.setter(#innerHtml, html), - returnValueForMissingStub: null); - @override - String get innerText => - (super.noSuchMethod(Invocation.getter(#innerText), returnValue: '') - as String); - @override - set innerText(String? value) => - super.noSuchMethod(Invocation.setter(#innerText, value), - returnValueForMissingStub: null); - @override - _i2.ElementEvents get on => (super.noSuchMethod(Invocation.getter(#on), - returnValue: _FakeElementEvents_4()) as _i2.ElementEvents); - @override - int get offsetHeight => - (super.noSuchMethod(Invocation.getter(#offsetHeight), returnValue: 0) - as int); - @override - int get offsetLeft => - (super.noSuchMethod(Invocation.getter(#offsetLeft), returnValue: 0) - as int); - @override - int get offsetTop => - (super.noSuchMethod(Invocation.getter(#offsetTop), returnValue: 0) - as int); - @override - int get offsetWidth => - (super.noSuchMethod(Invocation.getter(#offsetWidth), returnValue: 0) - as int); - @override - int get scrollHeight => - (super.noSuchMethod(Invocation.getter(#scrollHeight), returnValue: 0) - as int); - @override - int get scrollLeft => - (super.noSuchMethod(Invocation.getter(#scrollLeft), returnValue: 0) - as int); - @override - set scrollLeft(int? value) => - super.noSuchMethod(Invocation.setter(#scrollLeft, value), - returnValueForMissingStub: null); - @override - int get scrollTop => - (super.noSuchMethod(Invocation.getter(#scrollTop), returnValue: 0) - as int); - @override - set scrollTop(int? value) => - super.noSuchMethod(Invocation.setter(#scrollTop, value), - returnValueForMissingStub: null); - @override - int get scrollWidth => - (super.noSuchMethod(Invocation.getter(#scrollWidth), returnValue: 0) - as int); - @override - String get contentEditable => - (super.noSuchMethod(Invocation.getter(#contentEditable), returnValue: '') - as String); - @override - set contentEditable(String? value) => - super.noSuchMethod(Invocation.setter(#contentEditable, value), - returnValueForMissingStub: null); - @override - set dir(String? value) => super.noSuchMethod(Invocation.setter(#dir, value), - returnValueForMissingStub: null); - @override - bool get draggable => - (super.noSuchMethod(Invocation.getter(#draggable), returnValue: false) - as bool); - @override - set draggable(bool? value) => - super.noSuchMethod(Invocation.setter(#draggable, value), - returnValueForMissingStub: null); - @override - bool get hidden => - (super.noSuchMethod(Invocation.getter(#hidden), returnValue: false) - as bool); - @override - set hidden(bool? value) => - super.noSuchMethod(Invocation.setter(#hidden, value), - returnValueForMissingStub: null); - @override - set inert(bool? value) => super.noSuchMethod(Invocation.setter(#inert, value), - returnValueForMissingStub: null); - @override - set inputMode(String? value) => - super.noSuchMethod(Invocation.setter(#inputMode, value), - returnValueForMissingStub: null); - @override - set lang(String? value) => super.noSuchMethod(Invocation.setter(#lang, value), - returnValueForMissingStub: null); - @override - set spellcheck(bool? value) => - super.noSuchMethod(Invocation.setter(#spellcheck, value), - returnValueForMissingStub: null); + set allow(String? value) => super.noSuchMethod( + Invocation.setter( + #allow, + value, + ), + returnValueForMissingStub: null, + ); + @override + set allowFullscreen(bool? value) => super.noSuchMethod( + Invocation.setter( + #allowFullscreen, + value, + ), + returnValueForMissingStub: null, + ); + @override + set allowPaymentRequest(bool? value) => super.noSuchMethod( + Invocation.setter( + #allowPaymentRequest, + value, + ), + returnValueForMissingStub: null, + ); + @override + set csp(String? value) => super.noSuchMethod( + Invocation.setter( + #csp, + value, + ), + returnValueForMissingStub: null, + ); + @override + set height(String? value) => super.noSuchMethod( + Invocation.setter( + #height, + value, + ), + returnValueForMissingStub: null, + ); + @override + set name(String? value) => super.noSuchMethod( + Invocation.setter( + #name, + value, + ), + returnValueForMissingStub: null, + ); + @override + set referrerPolicy(String? value) => super.noSuchMethod( + Invocation.setter( + #referrerPolicy, + value, + ), + returnValueForMissingStub: null, + ); + @override + set src(String? value) => super.noSuchMethod( + Invocation.setter( + #src, + value, + ), + returnValueForMissingStub: null, + ); + @override + set srcdoc(String? value) => super.noSuchMethod( + Invocation.setter( + #srcdoc, + value, + ), + returnValueForMissingStub: null, + ); + @override + set width(String? value) => super.noSuchMethod( + Invocation.setter( + #width, + value, + ), + returnValueForMissingStub: null, + ); + @override + set nonce(String? value) => super.noSuchMethod( + Invocation.setter( + #nonce, + value, + ), + returnValueForMissingStub: null, + ); + @override + Map get attributes => (super.noSuchMethod( + Invocation.getter(#attributes), + returnValue: {}, + ) as Map); + @override + set attributes(Map? value) => super.noSuchMethod( + Invocation.setter( + #attributes, + value, + ), + returnValueForMissingStub: null, + ); + @override + List<_i2.Element> get children => (super.noSuchMethod( + Invocation.getter(#children), + returnValue: <_i2.Element>[], + ) as List<_i2.Element>); + @override + set children(List<_i2.Element>? value) => super.noSuchMethod( + Invocation.setter( + #children, + value, + ), + returnValueForMissingStub: null, + ); + @override + _i2.CssClassSet get classes => (super.noSuchMethod( + Invocation.getter(#classes), + returnValue: _FakeCssClassSet_0( + this, + Invocation.getter(#classes), + ), + ) as _i2.CssClassSet); + @override + set classes(Iterable? value) => super.noSuchMethod( + Invocation.setter( + #classes, + value, + ), + returnValueForMissingStub: null, + ); + @override + Map get dataset => (super.noSuchMethod( + Invocation.getter(#dataset), + returnValue: {}, + ) as Map); + @override + set dataset(Map? value) => super.noSuchMethod( + Invocation.setter( + #dataset, + value, + ), + returnValueForMissingStub: null, + ); + @override + _i3.Rectangle get client => (super.noSuchMethod( + Invocation.getter(#client), + returnValue: _FakeRectangle_1( + this, + Invocation.getter(#client), + ), + ) as _i3.Rectangle); + @override + _i3.Rectangle get offset => (super.noSuchMethod( + Invocation.getter(#offset), + returnValue: _FakeRectangle_1( + this, + Invocation.getter(#offset), + ), + ) as _i3.Rectangle); + @override + String get localName => (super.noSuchMethod( + Invocation.getter(#localName), + returnValue: '', + ) as String); + @override + _i2.CssRect get contentEdge => (super.noSuchMethod( + Invocation.getter(#contentEdge), + returnValue: _FakeCssRect_2( + this, + Invocation.getter(#contentEdge), + ), + ) as _i2.CssRect); + @override + _i2.CssRect get paddingEdge => (super.noSuchMethod( + Invocation.getter(#paddingEdge), + returnValue: _FakeCssRect_2( + this, + Invocation.getter(#paddingEdge), + ), + ) as _i2.CssRect); + @override + _i2.CssRect get borderEdge => (super.noSuchMethod( + Invocation.getter(#borderEdge), + returnValue: _FakeCssRect_2( + this, + Invocation.getter(#borderEdge), + ), + ) as _i2.CssRect); + @override + _i2.CssRect get marginEdge => (super.noSuchMethod( + Invocation.getter(#marginEdge), + returnValue: _FakeCssRect_2( + this, + Invocation.getter(#marginEdge), + ), + ) as _i2.CssRect); + @override + _i3.Point get documentOffset => (super.noSuchMethod( + Invocation.getter(#documentOffset), + returnValue: _FakePoint_3( + this, + Invocation.getter(#documentOffset), + ), + ) as _i3.Point); + @override + set innerHtml(String? html) => super.noSuchMethod( + Invocation.setter( + #innerHtml, + html, + ), + returnValueForMissingStub: null, + ); + @override + String get innerText => (super.noSuchMethod( + Invocation.getter(#innerText), + returnValue: '', + ) as String); + @override + set innerText(String? value) => super.noSuchMethod( + Invocation.setter( + #innerText, + value, + ), + returnValueForMissingStub: null, + ); + @override + _i2.ElementEvents get on => (super.noSuchMethod( + Invocation.getter(#on), + returnValue: _FakeElementEvents_4( + this, + Invocation.getter(#on), + ), + ) as _i2.ElementEvents); + @override + int get offsetHeight => (super.noSuchMethod( + Invocation.getter(#offsetHeight), + returnValue: 0, + ) as int); + @override + int get offsetLeft => (super.noSuchMethod( + Invocation.getter(#offsetLeft), + returnValue: 0, + ) as int); + @override + int get offsetTop => (super.noSuchMethod( + Invocation.getter(#offsetTop), + returnValue: 0, + ) as int); + @override + int get offsetWidth => (super.noSuchMethod( + Invocation.getter(#offsetWidth), + returnValue: 0, + ) as int); + @override + int get scrollHeight => (super.noSuchMethod( + Invocation.getter(#scrollHeight), + returnValue: 0, + ) as int); + @override + int get scrollLeft => (super.noSuchMethod( + Invocation.getter(#scrollLeft), + returnValue: 0, + ) as int); + @override + set scrollLeft(int? value) => super.noSuchMethod( + Invocation.setter( + #scrollLeft, + value, + ), + returnValueForMissingStub: null, + ); + @override + int get scrollTop => (super.noSuchMethod( + Invocation.getter(#scrollTop), + returnValue: 0, + ) as int); + @override + set scrollTop(int? value) => super.noSuchMethod( + Invocation.setter( + #scrollTop, + value, + ), + returnValueForMissingStub: null, + ); + @override + int get scrollWidth => (super.noSuchMethod( + Invocation.getter(#scrollWidth), + returnValue: 0, + ) as int); + @override + String get contentEditable => (super.noSuchMethod( + Invocation.getter(#contentEditable), + returnValue: '', + ) as String); + @override + set contentEditable(String? value) => super.noSuchMethod( + Invocation.setter( + #contentEditable, + value, + ), + returnValueForMissingStub: null, + ); + @override + set dir(String? value) => super.noSuchMethod( + Invocation.setter( + #dir, + value, + ), + returnValueForMissingStub: null, + ); + @override + bool get draggable => (super.noSuchMethod( + Invocation.getter(#draggable), + returnValue: false, + ) as bool); + @override + set draggable(bool? value) => super.noSuchMethod( + Invocation.setter( + #draggable, + value, + ), + returnValueForMissingStub: null, + ); + @override + bool get hidden => (super.noSuchMethod( + Invocation.getter(#hidden), + returnValue: false, + ) as bool); + @override + set hidden(bool? value) => super.noSuchMethod( + Invocation.setter( + #hidden, + value, + ), + returnValueForMissingStub: null, + ); + @override + set inert(bool? value) => super.noSuchMethod( + Invocation.setter( + #inert, + value, + ), + returnValueForMissingStub: null, + ); + @override + set inputMode(String? value) => super.noSuchMethod( + Invocation.setter( + #inputMode, + value, + ), + returnValueForMissingStub: null, + ); + @override + set lang(String? value) => super.noSuchMethod( + Invocation.setter( + #lang, + value, + ), + returnValueForMissingStub: null, + ); + @override + set spellcheck(bool? value) => super.noSuchMethod( + Invocation.setter( + #spellcheck, + value, + ), + returnValueForMissingStub: null, + ); @override _i2.CssStyleDeclaration get style => (super.noSuchMethod( - Invocation.getter(#style), - returnValue: _FakeCssStyleDeclaration_5()) as _i2.CssStyleDeclaration); - @override - set tabIndex(int? value) => - super.noSuchMethod(Invocation.setter(#tabIndex, value), - returnValueForMissingStub: null); - @override - set title(String? value) => - super.noSuchMethod(Invocation.setter(#title, value), - returnValueForMissingStub: null); - @override - set translate(bool? value) => - super.noSuchMethod(Invocation.setter(#translate, value), - returnValueForMissingStub: null); - @override - String get className => - (super.noSuchMethod(Invocation.getter(#className), returnValue: '') - as String); - @override - set className(String? value) => - super.noSuchMethod(Invocation.setter(#className, value), - returnValueForMissingStub: null); - @override - int get clientHeight => - (super.noSuchMethod(Invocation.getter(#clientHeight), returnValue: 0) - as int); - @override - int get clientWidth => - (super.noSuchMethod(Invocation.getter(#clientWidth), returnValue: 0) - as int); - @override - String get id => - (super.noSuchMethod(Invocation.getter(#id), returnValue: '') as String); - @override - set id(String? value) => super.noSuchMethod(Invocation.setter(#id, value), - returnValueForMissingStub: null); - @override - set slot(String? value) => super.noSuchMethod(Invocation.setter(#slot, value), - returnValueForMissingStub: null); - @override - String get tagName => - (super.noSuchMethod(Invocation.getter(#tagName), returnValue: '') - as String); - @override - _i2.ElementStream<_i2.Event> get onAbort => - (super.noSuchMethod(Invocation.getter(#onAbort), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onBeforeCopy => - (super.noSuchMethod(Invocation.getter(#onBeforeCopy), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onBeforeCut => - (super.noSuchMethod(Invocation.getter(#onBeforeCut), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onBeforePaste => - (super.noSuchMethod(Invocation.getter(#onBeforePaste), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onBlur => - (super.noSuchMethod(Invocation.getter(#onBlur), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onCanPlay => - (super.noSuchMethod(Invocation.getter(#onCanPlay), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onCanPlayThrough => - (super.noSuchMethod(Invocation.getter(#onCanPlayThrough), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onChange => - (super.noSuchMethod(Invocation.getter(#onChange), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.MouseEvent> get onClick => - (super.noSuchMethod(Invocation.getter(#onClick), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.MouseEvent> get onContextMenu => - (super.noSuchMethod(Invocation.getter(#onContextMenu), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.ClipboardEvent> get onCopy => - (super.noSuchMethod(Invocation.getter(#onCopy), - returnValue: _FakeElementStream_6<_i2.ClipboardEvent>()) - as _i2.ElementStream<_i2.ClipboardEvent>); - @override - _i2.ElementStream<_i2.ClipboardEvent> get onCut => - (super.noSuchMethod(Invocation.getter(#onCut), - returnValue: _FakeElementStream_6<_i2.ClipboardEvent>()) - as _i2.ElementStream<_i2.ClipboardEvent>); - @override - _i2.ElementStream<_i2.Event> get onDoubleClick => - (super.noSuchMethod(Invocation.getter(#onDoubleClick), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.MouseEvent> get onDrag => - (super.noSuchMethod(Invocation.getter(#onDrag), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.MouseEvent> get onDragEnd => - (super.noSuchMethod(Invocation.getter(#onDragEnd), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.MouseEvent> get onDragEnter => - (super.noSuchMethod(Invocation.getter(#onDragEnter), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.MouseEvent> get onDragLeave => - (super.noSuchMethod(Invocation.getter(#onDragLeave), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.MouseEvent> get onDragOver => - (super.noSuchMethod(Invocation.getter(#onDragOver), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.MouseEvent> get onDragStart => - (super.noSuchMethod(Invocation.getter(#onDragStart), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.MouseEvent> get onDrop => - (super.noSuchMethod(Invocation.getter(#onDrop), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.Event> get onDurationChange => - (super.noSuchMethod(Invocation.getter(#onDurationChange), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onEmptied => - (super.noSuchMethod(Invocation.getter(#onEmptied), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onEnded => - (super.noSuchMethod(Invocation.getter(#onEnded), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onError => - (super.noSuchMethod(Invocation.getter(#onError), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onFocus => - (super.noSuchMethod(Invocation.getter(#onFocus), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onInput => - (super.noSuchMethod(Invocation.getter(#onInput), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onInvalid => - (super.noSuchMethod(Invocation.getter(#onInvalid), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.KeyboardEvent> get onKeyDown => - (super.noSuchMethod(Invocation.getter(#onKeyDown), - returnValue: _FakeElementStream_6<_i2.KeyboardEvent>()) - as _i2.ElementStream<_i2.KeyboardEvent>); - @override - _i2.ElementStream<_i2.KeyboardEvent> get onKeyPress => - (super.noSuchMethod(Invocation.getter(#onKeyPress), - returnValue: _FakeElementStream_6<_i2.KeyboardEvent>()) - as _i2.ElementStream<_i2.KeyboardEvent>); - @override - _i2.ElementStream<_i2.KeyboardEvent> get onKeyUp => - (super.noSuchMethod(Invocation.getter(#onKeyUp), - returnValue: _FakeElementStream_6<_i2.KeyboardEvent>()) - as _i2.ElementStream<_i2.KeyboardEvent>); - @override - _i2.ElementStream<_i2.Event> get onLoad => - (super.noSuchMethod(Invocation.getter(#onLoad), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onLoadedData => - (super.noSuchMethod(Invocation.getter(#onLoadedData), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onLoadedMetadata => - (super.noSuchMethod(Invocation.getter(#onLoadedMetadata), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.MouseEvent> get onMouseDown => - (super.noSuchMethod(Invocation.getter(#onMouseDown), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.MouseEvent> get onMouseEnter => - (super.noSuchMethod(Invocation.getter(#onMouseEnter), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.MouseEvent> get onMouseLeave => - (super.noSuchMethod(Invocation.getter(#onMouseLeave), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.MouseEvent> get onMouseMove => - (super.noSuchMethod(Invocation.getter(#onMouseMove), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.MouseEvent> get onMouseOut => - (super.noSuchMethod(Invocation.getter(#onMouseOut), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.MouseEvent> get onMouseOver => - (super.noSuchMethod(Invocation.getter(#onMouseOver), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.MouseEvent> get onMouseUp => - (super.noSuchMethod(Invocation.getter(#onMouseUp), - returnValue: _FakeElementStream_6<_i2.MouseEvent>()) - as _i2.ElementStream<_i2.MouseEvent>); - @override - _i2.ElementStream<_i2.WheelEvent> get onMouseWheel => - (super.noSuchMethod(Invocation.getter(#onMouseWheel), - returnValue: _FakeElementStream_6<_i2.WheelEvent>()) - as _i2.ElementStream<_i2.WheelEvent>); - @override - _i2.ElementStream<_i2.ClipboardEvent> get onPaste => - (super.noSuchMethod(Invocation.getter(#onPaste), - returnValue: _FakeElementStream_6<_i2.ClipboardEvent>()) - as _i2.ElementStream<_i2.ClipboardEvent>); - @override - _i2.ElementStream<_i2.Event> get onPause => - (super.noSuchMethod(Invocation.getter(#onPause), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onPlay => - (super.noSuchMethod(Invocation.getter(#onPlay), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onPlaying => - (super.noSuchMethod(Invocation.getter(#onPlaying), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onRateChange => - (super.noSuchMethod(Invocation.getter(#onRateChange), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onReset => - (super.noSuchMethod(Invocation.getter(#onReset), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onResize => - (super.noSuchMethod(Invocation.getter(#onResize), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onScroll => - (super.noSuchMethod(Invocation.getter(#onScroll), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onSearch => - (super.noSuchMethod(Invocation.getter(#onSearch), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onSeeked => - (super.noSuchMethod(Invocation.getter(#onSeeked), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onSeeking => - (super.noSuchMethod(Invocation.getter(#onSeeking), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onSelect => - (super.noSuchMethod(Invocation.getter(#onSelect), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onSelectStart => - (super.noSuchMethod(Invocation.getter(#onSelectStart), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onStalled => - (super.noSuchMethod(Invocation.getter(#onStalled), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onSubmit => - (super.noSuchMethod(Invocation.getter(#onSubmit), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onSuspend => - (super.noSuchMethod(Invocation.getter(#onSuspend), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onTimeUpdate => - (super.noSuchMethod(Invocation.getter(#onTimeUpdate), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.TouchEvent> get onTouchCancel => - (super.noSuchMethod(Invocation.getter(#onTouchCancel), - returnValue: _FakeElementStream_6<_i2.TouchEvent>()) - as _i2.ElementStream<_i2.TouchEvent>); - @override - _i2.ElementStream<_i2.TouchEvent> get onTouchEnd => - (super.noSuchMethod(Invocation.getter(#onTouchEnd), - returnValue: _FakeElementStream_6<_i2.TouchEvent>()) - as _i2.ElementStream<_i2.TouchEvent>); - @override - _i2.ElementStream<_i2.TouchEvent> get onTouchEnter => - (super.noSuchMethod(Invocation.getter(#onTouchEnter), - returnValue: _FakeElementStream_6<_i2.TouchEvent>()) - as _i2.ElementStream<_i2.TouchEvent>); - @override - _i2.ElementStream<_i2.TouchEvent> get onTouchLeave => - (super.noSuchMethod(Invocation.getter(#onTouchLeave), - returnValue: _FakeElementStream_6<_i2.TouchEvent>()) - as _i2.ElementStream<_i2.TouchEvent>); - @override - _i2.ElementStream<_i2.TouchEvent> get onTouchMove => - (super.noSuchMethod(Invocation.getter(#onTouchMove), - returnValue: _FakeElementStream_6<_i2.TouchEvent>()) - as _i2.ElementStream<_i2.TouchEvent>); - @override - _i2.ElementStream<_i2.TouchEvent> get onTouchStart => - (super.noSuchMethod(Invocation.getter(#onTouchStart), - returnValue: _FakeElementStream_6<_i2.TouchEvent>()) - as _i2.ElementStream<_i2.TouchEvent>); + Invocation.getter(#style), + returnValue: _FakeCssStyleDeclaration_5( + this, + Invocation.getter(#style), + ), + ) as _i2.CssStyleDeclaration); + @override + set tabIndex(int? value) => super.noSuchMethod( + Invocation.setter( + #tabIndex, + value, + ), + returnValueForMissingStub: null, + ); + @override + set title(String? value) => super.noSuchMethod( + Invocation.setter( + #title, + value, + ), + returnValueForMissingStub: null, + ); + @override + set translate(bool? value) => super.noSuchMethod( + Invocation.setter( + #translate, + value, + ), + returnValueForMissingStub: null, + ); + @override + String get className => (super.noSuchMethod( + Invocation.getter(#className), + returnValue: '', + ) as String); + @override + set className(String? value) => super.noSuchMethod( + Invocation.setter( + #className, + value, + ), + returnValueForMissingStub: null, + ); + @override + int get clientHeight => (super.noSuchMethod( + Invocation.getter(#clientHeight), + returnValue: 0, + ) as int); + @override + int get clientWidth => (super.noSuchMethod( + Invocation.getter(#clientWidth), + returnValue: 0, + ) as int); + @override + String get id => (super.noSuchMethod( + Invocation.getter(#id), + returnValue: '', + ) as String); + @override + set id(String? value) => super.noSuchMethod( + Invocation.setter( + #id, + value, + ), + returnValueForMissingStub: null, + ); + @override + set slot(String? value) => super.noSuchMethod( + Invocation.setter( + #slot, + value, + ), + returnValueForMissingStub: null, + ); + @override + String get tagName => (super.noSuchMethod( + Invocation.getter(#tagName), + returnValue: '', + ) as String); + @override + _i2.ElementStream<_i2.Event> get onAbort => (super.noSuchMethod( + Invocation.getter(#onAbort), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onAbort), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onBeforeCopy => (super.noSuchMethod( + Invocation.getter(#onBeforeCopy), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onBeforeCopy), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onBeforeCut => (super.noSuchMethod( + Invocation.getter(#onBeforeCut), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onBeforeCut), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onBeforePaste => (super.noSuchMethod( + Invocation.getter(#onBeforePaste), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onBeforePaste), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onBlur => (super.noSuchMethod( + Invocation.getter(#onBlur), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onBlur), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onCanPlay => (super.noSuchMethod( + Invocation.getter(#onCanPlay), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onCanPlay), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onCanPlayThrough => (super.noSuchMethod( + Invocation.getter(#onCanPlayThrough), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onCanPlayThrough), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onChange => (super.noSuchMethod( + Invocation.getter(#onChange), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onChange), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.MouseEvent> get onClick => (super.noSuchMethod( + Invocation.getter(#onClick), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onClick), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onContextMenu => (super.noSuchMethod( + Invocation.getter(#onContextMenu), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onContextMenu), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.ClipboardEvent> get onCopy => (super.noSuchMethod( + Invocation.getter(#onCopy), + returnValue: _FakeElementStream_6<_i2.ClipboardEvent>( + this, + Invocation.getter(#onCopy), + ), + ) as _i2.ElementStream<_i2.ClipboardEvent>); + @override + _i2.ElementStream<_i2.ClipboardEvent> get onCut => (super.noSuchMethod( + Invocation.getter(#onCut), + returnValue: _FakeElementStream_6<_i2.ClipboardEvent>( + this, + Invocation.getter(#onCut), + ), + ) as _i2.ElementStream<_i2.ClipboardEvent>); + @override + _i2.ElementStream<_i2.Event> get onDoubleClick => (super.noSuchMethod( + Invocation.getter(#onDoubleClick), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onDoubleClick), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDrag => (super.noSuchMethod( + Invocation.getter(#onDrag), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onDrag), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDragEnd => (super.noSuchMethod( + Invocation.getter(#onDragEnd), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onDragEnd), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDragEnter => (super.noSuchMethod( + Invocation.getter(#onDragEnter), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onDragEnter), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDragLeave => (super.noSuchMethod( + Invocation.getter(#onDragLeave), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onDragLeave), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDragOver => (super.noSuchMethod( + Invocation.getter(#onDragOver), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onDragOver), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDragStart => (super.noSuchMethod( + Invocation.getter(#onDragStart), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onDragStart), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onDrop => (super.noSuchMethod( + Invocation.getter(#onDrop), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onDrop), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.Event> get onDurationChange => (super.noSuchMethod( + Invocation.getter(#onDurationChange), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onDurationChange), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onEmptied => (super.noSuchMethod( + Invocation.getter(#onEmptied), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onEmptied), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onEnded => (super.noSuchMethod( + Invocation.getter(#onEnded), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onEnded), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onError => (super.noSuchMethod( + Invocation.getter(#onError), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onError), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onFocus => (super.noSuchMethod( + Invocation.getter(#onFocus), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onFocus), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onInput => (super.noSuchMethod( + Invocation.getter(#onInput), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onInput), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onInvalid => (super.noSuchMethod( + Invocation.getter(#onInvalid), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onInvalid), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.KeyboardEvent> get onKeyDown => (super.noSuchMethod( + Invocation.getter(#onKeyDown), + returnValue: _FakeElementStream_6<_i2.KeyboardEvent>( + this, + Invocation.getter(#onKeyDown), + ), + ) as _i2.ElementStream<_i2.KeyboardEvent>); + @override + _i2.ElementStream<_i2.KeyboardEvent> get onKeyPress => (super.noSuchMethod( + Invocation.getter(#onKeyPress), + returnValue: _FakeElementStream_6<_i2.KeyboardEvent>( + this, + Invocation.getter(#onKeyPress), + ), + ) as _i2.ElementStream<_i2.KeyboardEvent>); + @override + _i2.ElementStream<_i2.KeyboardEvent> get onKeyUp => (super.noSuchMethod( + Invocation.getter(#onKeyUp), + returnValue: _FakeElementStream_6<_i2.KeyboardEvent>( + this, + Invocation.getter(#onKeyUp), + ), + ) as _i2.ElementStream<_i2.KeyboardEvent>); + @override + _i2.ElementStream<_i2.Event> get onLoad => (super.noSuchMethod( + Invocation.getter(#onLoad), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onLoad), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onLoadedData => (super.noSuchMethod( + Invocation.getter(#onLoadedData), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onLoadedData), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onLoadedMetadata => (super.noSuchMethod( + Invocation.getter(#onLoadedMetadata), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onLoadedMetadata), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseDown => (super.noSuchMethod( + Invocation.getter(#onMouseDown), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onMouseDown), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseEnter => (super.noSuchMethod( + Invocation.getter(#onMouseEnter), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onMouseEnter), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseLeave => (super.noSuchMethod( + Invocation.getter(#onMouseLeave), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onMouseLeave), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseMove => (super.noSuchMethod( + Invocation.getter(#onMouseMove), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onMouseMove), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseOut => (super.noSuchMethod( + Invocation.getter(#onMouseOut), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onMouseOut), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseOver => (super.noSuchMethod( + Invocation.getter(#onMouseOver), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onMouseOver), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.MouseEvent> get onMouseUp => (super.noSuchMethod( + Invocation.getter(#onMouseUp), + returnValue: _FakeElementStream_6<_i2.MouseEvent>( + this, + Invocation.getter(#onMouseUp), + ), + ) as _i2.ElementStream<_i2.MouseEvent>); + @override + _i2.ElementStream<_i2.WheelEvent> get onMouseWheel => (super.noSuchMethod( + Invocation.getter(#onMouseWheel), + returnValue: _FakeElementStream_6<_i2.WheelEvent>( + this, + Invocation.getter(#onMouseWheel), + ), + ) as _i2.ElementStream<_i2.WheelEvent>); + @override + _i2.ElementStream<_i2.ClipboardEvent> get onPaste => (super.noSuchMethod( + Invocation.getter(#onPaste), + returnValue: _FakeElementStream_6<_i2.ClipboardEvent>( + this, + Invocation.getter(#onPaste), + ), + ) as _i2.ElementStream<_i2.ClipboardEvent>); + @override + _i2.ElementStream<_i2.Event> get onPause => (super.noSuchMethod( + Invocation.getter(#onPause), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onPause), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onPlay => (super.noSuchMethod( + Invocation.getter(#onPlay), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onPlay), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onPlaying => (super.noSuchMethod( + Invocation.getter(#onPlaying), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onPlaying), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onRateChange => (super.noSuchMethod( + Invocation.getter(#onRateChange), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onRateChange), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onReset => (super.noSuchMethod( + Invocation.getter(#onReset), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onReset), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onResize => (super.noSuchMethod( + Invocation.getter(#onResize), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onResize), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onScroll => (super.noSuchMethod( + Invocation.getter(#onScroll), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onScroll), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSearch => (super.noSuchMethod( + Invocation.getter(#onSearch), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onSearch), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSeeked => (super.noSuchMethod( + Invocation.getter(#onSeeked), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onSeeked), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSeeking => (super.noSuchMethod( + Invocation.getter(#onSeeking), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onSeeking), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSelect => (super.noSuchMethod( + Invocation.getter(#onSelect), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onSelect), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSelectStart => (super.noSuchMethod( + Invocation.getter(#onSelectStart), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onSelectStart), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onStalled => (super.noSuchMethod( + Invocation.getter(#onStalled), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onStalled), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSubmit => (super.noSuchMethod( + Invocation.getter(#onSubmit), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onSubmit), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onSuspend => (super.noSuchMethod( + Invocation.getter(#onSuspend), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onSuspend), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onTimeUpdate => (super.noSuchMethod( + Invocation.getter(#onTimeUpdate), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onTimeUpdate), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.TouchEvent> get onTouchCancel => (super.noSuchMethod( + Invocation.getter(#onTouchCancel), + returnValue: _FakeElementStream_6<_i2.TouchEvent>( + this, + Invocation.getter(#onTouchCancel), + ), + ) as _i2.ElementStream<_i2.TouchEvent>); + @override + _i2.ElementStream<_i2.TouchEvent> get onTouchEnd => (super.noSuchMethod( + Invocation.getter(#onTouchEnd), + returnValue: _FakeElementStream_6<_i2.TouchEvent>( + this, + Invocation.getter(#onTouchEnd), + ), + ) as _i2.ElementStream<_i2.TouchEvent>); + @override + _i2.ElementStream<_i2.TouchEvent> get onTouchEnter => (super.noSuchMethod( + Invocation.getter(#onTouchEnter), + returnValue: _FakeElementStream_6<_i2.TouchEvent>( + this, + Invocation.getter(#onTouchEnter), + ), + ) as _i2.ElementStream<_i2.TouchEvent>); + @override + _i2.ElementStream<_i2.TouchEvent> get onTouchLeave => (super.noSuchMethod( + Invocation.getter(#onTouchLeave), + returnValue: _FakeElementStream_6<_i2.TouchEvent>( + this, + Invocation.getter(#onTouchLeave), + ), + ) as _i2.ElementStream<_i2.TouchEvent>); + @override + _i2.ElementStream<_i2.TouchEvent> get onTouchMove => (super.noSuchMethod( + Invocation.getter(#onTouchMove), + returnValue: _FakeElementStream_6<_i2.TouchEvent>( + this, + Invocation.getter(#onTouchMove), + ), + ) as _i2.ElementStream<_i2.TouchEvent>); + @override + _i2.ElementStream<_i2.TouchEvent> get onTouchStart => (super.noSuchMethod( + Invocation.getter(#onTouchStart), + returnValue: _FakeElementStream_6<_i2.TouchEvent>( + this, + Invocation.getter(#onTouchStart), + ), + ) as _i2.ElementStream<_i2.TouchEvent>); @override _i2.ElementStream<_i2.TransitionEvent> get onTransitionEnd => - (super.noSuchMethod(Invocation.getter(#onTransitionEnd), - returnValue: _FakeElementStream_6<_i2.TransitionEvent>()) - as _i2.ElementStream<_i2.TransitionEvent>); - @override - _i2.ElementStream<_i2.Event> get onVolumeChange => - (super.noSuchMethod(Invocation.getter(#onVolumeChange), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onWaiting => - (super.noSuchMethod(Invocation.getter(#onWaiting), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onFullscreenChange => - (super.noSuchMethod(Invocation.getter(#onFullscreenChange), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.Event> get onFullscreenError => - (super.noSuchMethod(Invocation.getter(#onFullscreenError), - returnValue: _FakeElementStream_6<_i2.Event>()) - as _i2.ElementStream<_i2.Event>); - @override - _i2.ElementStream<_i2.WheelEvent> get onWheel => - (super.noSuchMethod(Invocation.getter(#onWheel), - returnValue: _FakeElementStream_6<_i2.WheelEvent>()) - as _i2.ElementStream<_i2.WheelEvent>); - @override - List<_i2.Node> get nodes => - (super.noSuchMethod(Invocation.getter(#nodes), returnValue: <_i2.Node>[]) - as List<_i2.Node>); - @override - set nodes(Iterable<_i2.Node>? value) => - super.noSuchMethod(Invocation.setter(#nodes, value), - returnValueForMissingStub: null); - @override - List<_i2.Node> get childNodes => - (super.noSuchMethod(Invocation.getter(#childNodes), - returnValue: <_i2.Node>[]) as List<_i2.Node>); - @override - int get nodeType => - (super.noSuchMethod(Invocation.getter(#nodeType), returnValue: 0) as int); - @override - set text(String? value) => super.noSuchMethod(Invocation.setter(#text, value), - returnValueForMissingStub: null); - @override - String? getAttribute(String? name) => - (super.noSuchMethod(Invocation.method(#getAttribute, [name])) as String?); - @override - String? getAttributeNS(String? namespaceURI, String? name) => (super.noSuchMethod( - Invocation.method(#getAttributeNS, [namespaceURI, name])) as String?); - @override - bool hasAttribute(String? name) => - (super.noSuchMethod(Invocation.method(#hasAttribute, [name]), - returnValue: false) as bool); - @override - bool hasAttributeNS(String? namespaceURI, String? name) => (super - .noSuchMethod(Invocation.method(#hasAttributeNS, [namespaceURI, name]), - returnValue: false) as bool); - @override - void removeAttribute(String? name) => - super.noSuchMethod(Invocation.method(#removeAttribute, [name]), - returnValueForMissingStub: null); - @override - void removeAttributeNS(String? namespaceURI, String? name) => super - .noSuchMethod(Invocation.method(#removeAttributeNS, [namespaceURI, name]), - returnValueForMissingStub: null); - @override - void setAttribute(String? name, Object? value) => - super.noSuchMethod(Invocation.method(#setAttribute, [name, value]), - returnValueForMissingStub: null); - @override - void setAttributeNS(String? namespaceURI, String? name, Object? value) => + Invocation.getter(#onTransitionEnd), + returnValue: _FakeElementStream_6<_i2.TransitionEvent>( + this, + Invocation.getter(#onTransitionEnd), + ), + ) as _i2.ElementStream<_i2.TransitionEvent>); + @override + _i2.ElementStream<_i2.Event> get onVolumeChange => (super.noSuchMethod( + Invocation.getter(#onVolumeChange), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onVolumeChange), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onWaiting => (super.noSuchMethod( + Invocation.getter(#onWaiting), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onWaiting), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onFullscreenChange => (super.noSuchMethod( + Invocation.getter(#onFullscreenChange), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onFullscreenChange), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.Event> get onFullscreenError => (super.noSuchMethod( + Invocation.getter(#onFullscreenError), + returnValue: _FakeElementStream_6<_i2.Event>( + this, + Invocation.getter(#onFullscreenError), + ), + ) as _i2.ElementStream<_i2.Event>); + @override + _i2.ElementStream<_i2.WheelEvent> get onWheel => (super.noSuchMethod( + Invocation.getter(#onWheel), + returnValue: _FakeElementStream_6<_i2.WheelEvent>( + this, + Invocation.getter(#onWheel), + ), + ) as _i2.ElementStream<_i2.WheelEvent>); + @override + List<_i2.Node> get nodes => (super.noSuchMethod( + Invocation.getter(#nodes), + returnValue: <_i2.Node>[], + ) as List<_i2.Node>); + @override + set nodes(Iterable<_i2.Node>? value) => super.noSuchMethod( + Invocation.setter( + #nodes, + value, + ), + returnValueForMissingStub: null, + ); + @override + List<_i2.Node> get childNodes => (super.noSuchMethod( + Invocation.getter(#childNodes), + returnValue: <_i2.Node>[], + ) as List<_i2.Node>); + @override + int get nodeType => (super.noSuchMethod( + Invocation.getter(#nodeType), + returnValue: 0, + ) as int); + @override + set text(String? value) => super.noSuchMethod( + Invocation.setter( + #text, + value, + ), + returnValueForMissingStub: null, + ); + @override + String? getAttribute(String? name) => (super.noSuchMethod(Invocation.method( + #getAttribute, + [name], + )) as String?); + @override + String? getAttributeNS( + String? namespaceURI, + String? name, + ) => + (super.noSuchMethod(Invocation.method( + #getAttributeNS, + [ + namespaceURI, + name, + ], + )) as String?); + @override + bool hasAttribute(String? name) => (super.noSuchMethod( + Invocation.method( + #hasAttribute, + [name], + ), + returnValue: false, + ) as bool); + @override + bool hasAttributeNS( + String? namespaceURI, + String? name, + ) => + (super.noSuchMethod( + Invocation.method( + #hasAttributeNS, + [ + namespaceURI, + name, + ], + ), + returnValue: false, + ) as bool); + @override + void removeAttribute(String? name) => super.noSuchMethod( + Invocation.method( + #removeAttribute, + [name], + ), + returnValueForMissingStub: null, + ); + @override + void removeAttributeNS( + String? namespaceURI, + String? name, + ) => + super.noSuchMethod( + Invocation.method( + #removeAttributeNS, + [ + namespaceURI, + name, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setAttribute( + String? name, + Object? value, + ) => + super.noSuchMethod( + Invocation.method( + #setAttribute, + [ + name, + value, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setAttributeNS( + String? namespaceURI, + String? name, + Object? value, + ) => super.noSuchMethod( - Invocation.method(#setAttributeNS, [namespaceURI, name, value]), - returnValueForMissingStub: null); + Invocation.method( + #setAttributeNS, + [ + namespaceURI, + name, + value, + ], + ), + returnValueForMissingStub: null, + ); @override _i2.ElementList querySelectorAll( String? selectors) => - (super.noSuchMethod(Invocation.method(#querySelectorAll, [selectors]), - returnValue: _FakeElementList_7()) as _i2.ElementList); + (super.noSuchMethod( + Invocation.method( + #querySelectorAll, + [selectors], + ), + returnValue: _FakeElementList_7( + this, + Invocation.method( + #querySelectorAll, + [selectors], + ), + ), + ) as _i2.ElementList); @override _i6.Future<_i2.ScrollState> setApplyScroll(String? nativeScrollBehavior) => (super.noSuchMethod( - Invocation.method(#setApplyScroll, [nativeScrollBehavior]), - returnValue: Future<_i2.ScrollState>.value(_FakeScrollState_8())) - as _i6.Future<_i2.ScrollState>); + Invocation.method( + #setApplyScroll, + [nativeScrollBehavior], + ), + returnValue: _i6.Future<_i2.ScrollState>.value(_FakeScrollState_8( + this, + Invocation.method( + #setApplyScroll, + [nativeScrollBehavior], + ), + )), + ) as _i6.Future<_i2.ScrollState>); @override _i6.Future<_i2.ScrollState> setDistributeScroll( String? nativeScrollBehavior) => (super.noSuchMethod( - Invocation.method(#setDistributeScroll, [nativeScrollBehavior]), - returnValue: Future<_i2.ScrollState>.value(_FakeScrollState_8())) - as _i6.Future<_i2.ScrollState>); + Invocation.method( + #setDistributeScroll, + [nativeScrollBehavior], + ), + returnValue: _i6.Future<_i2.ScrollState>.value(_FakeScrollState_8( + this, + Invocation.method( + #setDistributeScroll, + [nativeScrollBehavior], + ), + )), + ) as _i6.Future<_i2.ScrollState>); @override - Map getNamespacedAttributes(String? namespace) => (super - .noSuchMethod(Invocation.method(#getNamespacedAttributes, [namespace]), - returnValue: {}) as Map); + Map getNamespacedAttributes(String? namespace) => + (super.noSuchMethod( + Invocation.method( + #getNamespacedAttributes, + [namespace], + ), + returnValue: {}, + ) as Map); @override _i2.CssStyleDeclaration getComputedStyle([String? pseudoElement]) => - (super.noSuchMethod(Invocation.method(#getComputedStyle, [pseudoElement]), - returnValue: _FakeCssStyleDeclaration_5()) - as _i2.CssStyleDeclaration); - @override - void appendText(String? text) => - super.noSuchMethod(Invocation.method(#appendText, [text]), - returnValueForMissingStub: null); - @override - void appendHtml(String? text, - {_i2.NodeValidator? validator, - _i2.NodeTreeSanitizer? treeSanitizer}) => + (super.noSuchMethod( + Invocation.method( + #getComputedStyle, + [pseudoElement], + ), + returnValue: _FakeCssStyleDeclaration_5( + this, + Invocation.method( + #getComputedStyle, + [pseudoElement], + ), + ), + ) as _i2.CssStyleDeclaration); + @override + void appendText(String? text) => super.noSuchMethod( + Invocation.method( + #appendText, + [text], + ), + returnValueForMissingStub: null, + ); + @override + void appendHtml( + String? text, { + _i2.NodeValidator? validator, + _i2.NodeTreeSanitizer? treeSanitizer, + }) => super.noSuchMethod( - Invocation.method(#appendHtml, [text], - {#validator: validator, #treeSanitizer: treeSanitizer}), - returnValueForMissingStub: null); - @override - void attached() => super.noSuchMethod(Invocation.method(#attached, []), - returnValueForMissingStub: null); - @override - void detached() => super.noSuchMethod(Invocation.method(#detached, []), - returnValueForMissingStub: null); - @override - void enteredView() => super.noSuchMethod(Invocation.method(#enteredView, []), - returnValueForMissingStub: null); - @override - List<_i3.Rectangle> getClientRects() => - (super.noSuchMethod(Invocation.method(#getClientRects, []), - returnValue: <_i3.Rectangle>[]) as List<_i3.Rectangle>); - @override - void leftView() => super.noSuchMethod(Invocation.method(#leftView, []), - returnValueForMissingStub: null); - @override - _i2.Animation animate(Iterable>? frames, - [dynamic timing]) => - (super.noSuchMethod(Invocation.method(#animate, [frames, timing]), - returnValue: _FakeAnimation_9()) as _i2.Animation); - @override - void attributeChanged(String? name, String? oldValue, String? newValue) => + Invocation.method( + #appendHtml, + [text], + { + #validator: validator, + #treeSanitizer: treeSanitizer, + }, + ), + returnValueForMissingStub: null, + ); + @override + void attached() => super.noSuchMethod( + Invocation.method( + #attached, + [], + ), + returnValueForMissingStub: null, + ); + @override + void detached() => super.noSuchMethod( + Invocation.method( + #detached, + [], + ), + returnValueForMissingStub: null, + ); + @override + void enteredView() => super.noSuchMethod( + Invocation.method( + #enteredView, + [], + ), + returnValueForMissingStub: null, + ); + @override + List<_i3.Rectangle> getClientRects() => (super.noSuchMethod( + Invocation.method( + #getClientRects, + [], + ), + returnValue: <_i3.Rectangle>[], + ) as List<_i3.Rectangle>); + @override + void leftView() => super.noSuchMethod( + Invocation.method( + #leftView, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i2.Animation animate( + Iterable>? frames, [ + dynamic timing, + ]) => + (super.noSuchMethod( + Invocation.method( + #animate, + [ + frames, + timing, + ], + ), + returnValue: _FakeAnimation_9( + this, + Invocation.method( + #animate, + [ + frames, + timing, + ], + ), + ), + ) as _i2.Animation); + @override + void attributeChanged( + String? name, + String? oldValue, + String? newValue, + ) => super.noSuchMethod( - Invocation.method(#attributeChanged, [name, oldValue, newValue]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); - @override - void scrollIntoView([_i2.ScrollAlignment? alignment]) => - super.noSuchMethod(Invocation.method(#scrollIntoView, [alignment]), - returnValueForMissingStub: null); - @override - void insertAdjacentText(String? where, String? text) => - super.noSuchMethod(Invocation.method(#insertAdjacentText, [where, text]), - returnValueForMissingStub: null); - @override - void insertAdjacentHtml(String? where, String? html, - {_i2.NodeValidator? validator, - _i2.NodeTreeSanitizer? treeSanitizer}) => + Invocation.method( + #attributeChanged, + [ + name, + oldValue, + newValue, + ], + ), + returnValueForMissingStub: null, + ); + @override + void scrollIntoView([_i2.ScrollAlignment? alignment]) => super.noSuchMethod( + Invocation.method( + #scrollIntoView, + [alignment], + ), + returnValueForMissingStub: null, + ); + @override + void insertAdjacentText( + String? where, + String? text, + ) => super.noSuchMethod( - Invocation.method(#insertAdjacentHtml, [where, html], - {#validator: validator, #treeSanitizer: treeSanitizer}), - returnValueForMissingStub: null); - @override - _i2.Element insertAdjacentElement(String? where, _i2.Element? element) => + Invocation.method( + #insertAdjacentText, + [ + where, + text, + ], + ), + returnValueForMissingStub: null, + ); + @override + void insertAdjacentHtml( + String? where, + String? html, { + _i2.NodeValidator? validator, + _i2.NodeTreeSanitizer? treeSanitizer, + }) => + super.noSuchMethod( + Invocation.method( + #insertAdjacentHtml, + [ + where, + html, + ], + { + #validator: validator, + #treeSanitizer: treeSanitizer, + }, + ), + returnValueForMissingStub: null, + ); + @override + _i2.Element insertAdjacentElement( + String? where, + _i2.Element? element, + ) => (super.noSuchMethod( - Invocation.method(#insertAdjacentElement, [where, element]), - returnValue: _FakeElement_10()) as _i2.Element); - @override - bool matches(String? selectors) => - (super.noSuchMethod(Invocation.method(#matches, [selectors]), - returnValue: false) as bool); - @override - bool matchesWithAncestors(String? selectors) => - (super.noSuchMethod(Invocation.method(#matchesWithAncestors, [selectors]), - returnValue: false) as bool); - @override - _i2.ShadowRoot createShadowRoot() => - (super.noSuchMethod(Invocation.method(#createShadowRoot, []), - returnValue: _FakeShadowRoot_11()) as _i2.ShadowRoot); - @override - _i3.Point offsetTo(_i2.Element? parent) => - (super.noSuchMethod(Invocation.method(#offsetTo, [parent]), - returnValue: _FakePoint_3()) as _i3.Point); - @override - _i2.DocumentFragment createFragment(String? html, - {_i2.NodeValidator? validator, - _i2.NodeTreeSanitizer? treeSanitizer}) => + Invocation.method( + #insertAdjacentElement, + [ + where, + element, + ], + ), + returnValue: _FakeElement_10( + this, + Invocation.method( + #insertAdjacentElement, + [ + where, + element, + ], + ), + ), + ) as _i2.Element); + @override + bool matches(String? selectors) => (super.noSuchMethod( + Invocation.method( + #matches, + [selectors], + ), + returnValue: false, + ) as bool); + @override + bool matchesWithAncestors(String? selectors) => (super.noSuchMethod( + Invocation.method( + #matchesWithAncestors, + [selectors], + ), + returnValue: false, + ) as bool); + @override + _i2.ShadowRoot createShadowRoot() => (super.noSuchMethod( + Invocation.method( + #createShadowRoot, + [], + ), + returnValue: _FakeShadowRoot_11( + this, + Invocation.method( + #createShadowRoot, + [], + ), + ), + ) as _i2.ShadowRoot); + @override + _i3.Point offsetTo(_i2.Element? parent) => (super.noSuchMethod( + Invocation.method( + #offsetTo, + [parent], + ), + returnValue: _FakePoint_3( + this, + Invocation.method( + #offsetTo, + [parent], + ), + ), + ) as _i3.Point); + @override + _i2.DocumentFragment createFragment( + String? html, { + _i2.NodeValidator? validator, + _i2.NodeTreeSanitizer? treeSanitizer, + }) => (super.noSuchMethod( - Invocation.method(#createFragment, [html], - {#validator: validator, #treeSanitizer: treeSanitizer}), - returnValue: _FakeDocumentFragment_12()) as _i2.DocumentFragment); - @override - void setInnerHtml(String? html, - {_i2.NodeValidator? validator, - _i2.NodeTreeSanitizer? treeSanitizer}) => + Invocation.method( + #createFragment, + [html], + { + #validator: validator, + #treeSanitizer: treeSanitizer, + }, + ), + returnValue: _FakeDocumentFragment_12( + this, + Invocation.method( + #createFragment, + [html], + { + #validator: validator, + #treeSanitizer: treeSanitizer, + }, + ), + ), + ) as _i2.DocumentFragment); + @override + void setInnerHtml( + String? html, { + _i2.NodeValidator? validator, + _i2.NodeTreeSanitizer? treeSanitizer, + }) => super.noSuchMethod( - Invocation.method(#setInnerHtml, [html], - {#validator: validator, #treeSanitizer: treeSanitizer}), - returnValueForMissingStub: null); - @override - void blur() => super.noSuchMethod(Invocation.method(#blur, []), - returnValueForMissingStub: null); - @override - void click() => super.noSuchMethod(Invocation.method(#click, []), - returnValueForMissingStub: null); - @override - void focus() => super.noSuchMethod(Invocation.method(#focus, []), - returnValueForMissingStub: null); + Invocation.method( + #setInnerHtml, + [html], + { + #validator: validator, + #treeSanitizer: treeSanitizer, + }, + ), + returnValueForMissingStub: null, + ); + @override + _i6.Future requestFullscreen([Map? options]) => + (super.noSuchMethod( + Invocation.method( + #requestFullscreen, + [options], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + @override + void blur() => super.noSuchMethod( + Invocation.method( + #blur, + [], + ), + returnValueForMissingStub: null, + ); + @override + void click() => super.noSuchMethod( + Invocation.method( + #click, + [], + ), + returnValueForMissingStub: null, + ); + @override + void focus() => super.noSuchMethod( + Invocation.method( + #focus, + [], + ), + returnValueForMissingStub: null, + ); @override _i2.ShadowRoot attachShadow(Map? shadowRootInitDict) => (super.noSuchMethod( - Invocation.method(#attachShadow, [shadowRootInitDict]), - returnValue: _FakeShadowRoot_11()) as _i2.ShadowRoot); + Invocation.method( + #attachShadow, + [shadowRootInitDict], + ), + returnValue: _FakeShadowRoot_11( + this, + Invocation.method( + #attachShadow, + [shadowRootInitDict], + ), + ), + ) as _i2.ShadowRoot); @override _i2.Element? closest(String? selectors) => - (super.noSuchMethod(Invocation.method(#closest, [selectors])) - as _i2.Element?); - @override - List<_i2.Animation> getAnimations() => - (super.noSuchMethod(Invocation.method(#getAnimations, []), - returnValue: <_i2.Animation>[]) as List<_i2.Animation>); - @override - List getAttributeNames() => - (super.noSuchMethod(Invocation.method(#getAttributeNames, []), - returnValue: []) as List); - @override - _i3.Rectangle getBoundingClientRect() => - (super.noSuchMethod(Invocation.method(#getBoundingClientRect, []), - returnValue: _FakeRectangle_1()) as _i3.Rectangle); - @override - List<_i2.Node> getDestinationInsertionPoints() => - (super.noSuchMethod(Invocation.method(#getDestinationInsertionPoints, []), - returnValue: <_i2.Node>[]) as List<_i2.Node>); - @override - List<_i2.Node> getElementsByClassName(String? classNames) => (super - .noSuchMethod(Invocation.method(#getElementsByClassName, [classNames]), - returnValue: <_i2.Node>[]) as List<_i2.Node>); - @override - bool hasPointerCapture(int? pointerId) => - (super.noSuchMethod(Invocation.method(#hasPointerCapture, [pointerId]), - returnValue: false) as bool); - @override - void releasePointerCapture(int? pointerId) => - super.noSuchMethod(Invocation.method(#releasePointerCapture, [pointerId]), - returnValueForMissingStub: null); - @override - void requestPointerLock() => - super.noSuchMethod(Invocation.method(#requestPointerLock, []), - returnValueForMissingStub: null); - @override - void scroll([dynamic options_OR_x, num? y]) => - super.noSuchMethod(Invocation.method(#scroll, [options_OR_x, y]), - returnValueForMissingStub: null); - @override - void scrollBy([dynamic options_OR_x, num? y]) => - super.noSuchMethod(Invocation.method(#scrollBy, [options_OR_x, y]), - returnValueForMissingStub: null); - @override - void scrollTo([dynamic options_OR_x, num? y]) => - super.noSuchMethod(Invocation.method(#scrollTo, [options_OR_x, y]), - returnValueForMissingStub: null); - @override - void setPointerCapture(int? pointerId) => - super.noSuchMethod(Invocation.method(#setPointerCapture, [pointerId]), - returnValueForMissingStub: null); - // TODO(ditman): Undo this manual change when the return type change to - // Future has propagated to stable. - /*@override - void requestFullscreen() => - super.noSuchMethod(Invocation.method(#requestFullscreen, []), - returnValueForMissingStub: null);*/ - @override - void after(Object? nodes) => - super.noSuchMethod(Invocation.method(#after, [nodes]), - returnValueForMissingStub: null); - @override - void before(Object? nodes) => - super.noSuchMethod(Invocation.method(#before, [nodes]), - returnValueForMissingStub: null); + (super.noSuchMethod(Invocation.method( + #closest, + [selectors], + )) as _i2.Element?); + @override + List<_i2.Animation> getAnimations() => (super.noSuchMethod( + Invocation.method( + #getAnimations, + [], + ), + returnValue: <_i2.Animation>[], + ) as List<_i2.Animation>); + @override + List getAttributeNames() => (super.noSuchMethod( + Invocation.method( + #getAttributeNames, + [], + ), + returnValue: [], + ) as List); + @override + _i3.Rectangle getBoundingClientRect() => (super.noSuchMethod( + Invocation.method( + #getBoundingClientRect, + [], + ), + returnValue: _FakeRectangle_1( + this, + Invocation.method( + #getBoundingClientRect, + [], + ), + ), + ) as _i3.Rectangle); + @override + List<_i2.Node> getDestinationInsertionPoints() => (super.noSuchMethod( + Invocation.method( + #getDestinationInsertionPoints, + [], + ), + returnValue: <_i2.Node>[], + ) as List<_i2.Node>); + @override + List<_i2.Node> getElementsByClassName(String? classNames) => + (super.noSuchMethod( + Invocation.method( + #getElementsByClassName, + [classNames], + ), + returnValue: <_i2.Node>[], + ) as List<_i2.Node>); + @override + bool hasPointerCapture(int? pointerId) => (super.noSuchMethod( + Invocation.method( + #hasPointerCapture, + [pointerId], + ), + returnValue: false, + ) as bool); + @override + void releasePointerCapture(int? pointerId) => super.noSuchMethod( + Invocation.method( + #releasePointerCapture, + [pointerId], + ), + returnValueForMissingStub: null, + ); + @override + void requestPointerLock() => super.noSuchMethod( + Invocation.method( + #requestPointerLock, + [], + ), + returnValueForMissingStub: null, + ); + @override + void scroll([ + dynamic options_OR_x, + num? y, + ]) => + super.noSuchMethod( + Invocation.method( + #scroll, + [ + options_OR_x, + y, + ], + ), + returnValueForMissingStub: null, + ); + @override + void scrollBy([ + dynamic options_OR_x, + num? y, + ]) => + super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + options_OR_x, + y, + ], + ), + returnValueForMissingStub: null, + ); + @override + void scrollIntoViewIfNeeded([bool? centerIfNeeded]) => super.noSuchMethod( + Invocation.method( + #scrollIntoViewIfNeeded, + [centerIfNeeded], + ), + returnValueForMissingStub: null, + ); + @override + void scrollTo([ + dynamic options_OR_x, + num? y, + ]) => + super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + options_OR_x, + y, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setPointerCapture(int? pointerId) => super.noSuchMethod( + Invocation.method( + #setPointerCapture, + [pointerId], + ), + returnValueForMissingStub: null, + ); + @override + void after(Object? nodes) => super.noSuchMethod( + Invocation.method( + #after, + [nodes], + ), + returnValueForMissingStub: null, + ); + @override + void before(Object? nodes) => super.noSuchMethod( + Invocation.method( + #before, + [nodes], + ), + returnValueForMissingStub: null, + ); @override _i2.Element? querySelector(String? selectors) => - (super.noSuchMethod(Invocation.method(#querySelector, [selectors])) - as _i2.Element?); - @override - void remove() => super.noSuchMethod(Invocation.method(#remove, []), - returnValueForMissingStub: null); - @override - _i2.Node replaceWith(_i2.Node? otherNode) => - (super.noSuchMethod(Invocation.method(#replaceWith, [otherNode]), - returnValue: _FakeNode_13()) as _i2.Node); - @override - void insertAllBefore(Iterable<_i2.Node>? newNodes, _i2.Node? refChild) => + (super.noSuchMethod(Invocation.method( + #querySelector, + [selectors], + )) as _i2.Element?); + @override + void remove() => super.noSuchMethod( + Invocation.method( + #remove, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i2.Node replaceWith(_i2.Node? otherNode) => (super.noSuchMethod( + Invocation.method( + #replaceWith, + [otherNode], + ), + returnValue: _FakeNode_13( + this, + Invocation.method( + #replaceWith, + [otherNode], + ), + ), + ) as _i2.Node); + @override + void insertAllBefore( + Iterable<_i2.Node>? newNodes, + _i2.Node? child, + ) => super.noSuchMethod( - Invocation.method(#insertAllBefore, [newNodes, refChild]), - returnValueForMissingStub: null); - @override - _i2.Node append(_i2.Node? node) => - (super.noSuchMethod(Invocation.method(#append, [node]), - returnValue: _FakeNode_13()) as _i2.Node); - @override - _i2.Node clone(bool? deep) => - (super.noSuchMethod(Invocation.method(#clone, [deep]), - returnValue: _FakeNode_13()) as _i2.Node); - @override - bool contains(_i2.Node? other) => - (super.noSuchMethod(Invocation.method(#contains, [other]), - returnValue: false) as bool); - @override - _i2.Node getRootNode([Map? options]) => - (super.noSuchMethod(Invocation.method(#getRootNode, [options]), - returnValue: _FakeNode_13()) as _i2.Node); - @override - bool hasChildNodes() => - (super.noSuchMethod(Invocation.method(#hasChildNodes, []), - returnValue: false) as bool); - @override - _i2.Node insertBefore(_i2.Node? node, _i2.Node? child) => - (super.noSuchMethod(Invocation.method(#insertBefore, [node, child]), - returnValue: _FakeNode_13()) as _i2.Node); - @override - void addEventListener(String? type, _i2.EventListener? listener, - [bool? useCapture]) => + Invocation.method( + #insertAllBefore, + [ + newNodes, + child, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i2.Node append(_i2.Node? node) => (super.noSuchMethod( + Invocation.method( + #append, + [node], + ), + returnValue: _FakeNode_13( + this, + Invocation.method( + #append, + [node], + ), + ), + ) as _i2.Node); + @override + _i2.Node clone(bool? deep) => (super.noSuchMethod( + Invocation.method( + #clone, + [deep], + ), + returnValue: _FakeNode_13( + this, + Invocation.method( + #clone, + [deep], + ), + ), + ) as _i2.Node); + @override + bool contains(_i2.Node? other) => (super.noSuchMethod( + Invocation.method( + #contains, + [other], + ), + returnValue: false, + ) as bool); + @override + _i2.Node getRootNode([Map? options]) => (super.noSuchMethod( + Invocation.method( + #getRootNode, + [options], + ), + returnValue: _FakeNode_13( + this, + Invocation.method( + #getRootNode, + [options], + ), + ), + ) as _i2.Node); + @override + bool hasChildNodes() => (super.noSuchMethod( + Invocation.method( + #hasChildNodes, + [], + ), + returnValue: false, + ) as bool); + @override + _i2.Node insertBefore( + _i2.Node? node, + _i2.Node? child, + ) => + (super.noSuchMethod( + Invocation.method( + #insertBefore, + [ + node, + child, + ], + ), + returnValue: _FakeNode_13( + this, + Invocation.method( + #insertBefore, + [ + node, + child, + ], + ), + ), + ) as _i2.Node); + @override + void addEventListener( + String? type, + _i2.EventListener? listener, [ + bool? useCapture, + ]) => super.noSuchMethod( - Invocation.method(#addEventListener, [type, listener, useCapture]), - returnValueForMissingStub: null); - @override - void removeEventListener(String? type, _i2.EventListener? listener, - [bool? useCapture]) => + Invocation.method( + #addEventListener, + [ + type, + listener, + useCapture, + ], + ), + returnValueForMissingStub: null, + ); + @override + void removeEventListener( + String? type, + _i2.EventListener? listener, [ + bool? useCapture, + ]) => super.noSuchMethod( - Invocation.method(#removeEventListener, [type, listener, useCapture]), - returnValueForMissingStub: null); - @override - bool dispatchEvent(_i2.Event? event) => - (super.noSuchMethod(Invocation.method(#dispatchEvent, [event]), - returnValue: false) as bool); + Invocation.method( + #removeEventListener, + [ + type, + listener, + useCapture, + ], + ), + returnValueForMissingStub: null, + ); + @override + bool dispatchEvent(_i2.Event? event) => (super.noSuchMethod( + Invocation.method( + #dispatchEvent, + [event], + ), + returnValue: false, + ) as bool); } /// A class which mocks [BuildContext]. @@ -1010,80 +2109,162 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { } @override - _i4.Widget get widget => (super.noSuchMethod(Invocation.getter(#widget), - returnValue: _FakeWidget_14()) as _i4.Widget); - @override - bool get debugDoingBuild => (super - .noSuchMethod(Invocation.getter(#debugDoingBuild), returnValue: false) - as bool); - @override - _i4.InheritedWidget dependOnInheritedElement(_i4.InheritedElement? ancestor, - {Object? aspect}) => + _i4.Widget get widget => (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_14( + 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_15( + this, Invocation.method( - #dependOnInheritedElement, [ancestor], {#aspect: aspect}), - returnValue: _FakeInheritedWidget_15()) as _i4.InheritedWidget); + #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 - _i5.DiagnosticsNode describeElement(String? name, - {_i5.DiagnosticsTreeStyle? style = - _i5.DiagnosticsTreeStyle.errorProperty}) => + 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(_i7.Notification? notification) => + super.noSuchMethod( + Invocation.method( + #dispatchNotification, + [notification], + ), + returnValueForMissingStub: null, + ); + @override + _i5.DiagnosticsNode describeElement( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => (super.noSuchMethod( - Invocation.method(#describeElement, [name], {#style: style}), - returnValue: _FakeDiagnosticsNode_16()) as _i5.DiagnosticsNode); - @override - _i5.DiagnosticsNode describeWidget(String? name, - {_i5.DiagnosticsTreeStyle? style = - _i5.DiagnosticsTreeStyle.errorProperty}) => + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_16( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + ) as _i5.DiagnosticsNode); + @override + _i5.DiagnosticsNode describeWidget( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => (super.noSuchMethod( - Invocation.method(#describeWidget, [name], {#style: style}), - returnValue: _FakeDiagnosticsNode_16()) as _i5.DiagnosticsNode); + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_16( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + ) as _i5.DiagnosticsNode); @override List<_i5.DiagnosticsNode> describeMissingAncestor( - {Type? expectedAncestorType}) => + {required Type? expectedAncestorType}) => (super.noSuchMethod( - Invocation.method(#describeMissingAncestor, [], - {#expectedAncestorType: expectedAncestorType}), - returnValue: <_i5.DiagnosticsNode>[]) as List<_i5.DiagnosticsNode>); + Invocation.method( + #describeMissingAncestor, + [], + {#expectedAncestorType: expectedAncestorType}, + ), + returnValue: <_i5.DiagnosticsNode>[], + ) as List<_i5.DiagnosticsNode>); @override _i5.DiagnosticsNode describeOwnershipChain(String? name) => - (super.noSuchMethod(Invocation.method(#describeOwnershipChain, [name]), - returnValue: _FakeDiagnosticsNode_16()) as _i5.DiagnosticsNode); - @override - String toString() => super.toString(); + (super.noSuchMethod( + Invocation.method( + #describeOwnershipChain, + [name], + ), + returnValue: _FakeDiagnosticsNode_16( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), + ), + ) as _i5.DiagnosticsNode); } /// A class which mocks [CreationParams]. /// /// See the documentation for Mockito's code generation for more information. -class MockCreationParams extends _i1.Mock implements _i7.CreationParams { +class MockCreationParams extends _i1.Mock implements _i8.CreationParams { MockCreationParams() { _i1.throwOnMissingStub(this); } @override - Set get javascriptChannelNames => - (super.noSuchMethod(Invocation.getter(#javascriptChannelNames), - returnValue: {}) as Set); + Set get javascriptChannelNames => (super.noSuchMethod( + Invocation.getter(#javascriptChannelNames), + returnValue: {}, + ) as Set); @override _i8.AutoMediaPlaybackPolicy get autoMediaPlaybackPolicy => - (super.noSuchMethod(Invocation.getter(#autoMediaPlaybackPolicy), - returnValue: _i8.AutoMediaPlaybackPolicy - .require_user_action_for_all_media_types) - as _i8.AutoMediaPlaybackPolicy); - @override - List<_i7.WebViewCookie> get cookies => - (super.noSuchMethod(Invocation.getter(#cookies), - returnValue: <_i7.WebViewCookie>[]) as List<_i7.WebViewCookie>); - @override - String toString() => super.toString(); + (super.noSuchMethod( + Invocation.getter(#autoMediaPlaybackPolicy), + returnValue: + _i8.AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ) as _i8.AutoMediaPlaybackPolicy); + @override + List<_i8.WebViewCookie> get cookies => (super.noSuchMethod( + Invocation.getter(#cookies), + returnValue: <_i8.WebViewCookie>[], + ) as List<_i8.WebViewCookie>); } /// A class which mocks [WebViewPlatformCallbacksHandler]. @@ -1096,29 +2277,53 @@ class MockWebViewPlatformCallbacksHandler extends _i1.Mock } @override - _i6.FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) => + _i6.FutureOr onNavigationRequest({ + required String? url, + required bool? isForMainFrame, + }) => (super.noSuchMethod( - Invocation.method(#onNavigationRequest, [], - {#url: url, #isForMainFrame: isForMainFrame}), - returnValue: Future.value(false)) as _i6.FutureOr); - @override - void onPageStarted(String? url) => - super.noSuchMethod(Invocation.method(#onPageStarted, [url]), - returnValueForMissingStub: null); - @override - void onPageFinished(String? url) => - super.noSuchMethod(Invocation.method(#onPageFinished, [url]), - returnValueForMissingStub: null); - @override - void onProgress(int? progress) => - super.noSuchMethod(Invocation.method(#onProgress, [progress]), - returnValueForMissingStub: null); - @override - void onWebResourceError(_i7.WebResourceError? error) => - super.noSuchMethod(Invocation.method(#onWebResourceError, [error]), - returnValueForMissingStub: null); - @override - String toString() => super.toString(); + Invocation.method( + #onNavigationRequest, + [], + { + #url: url, + #isForMainFrame: isForMainFrame, + }, + ), + returnValue: _i6.Future.value(false), + ) as _i6.FutureOr); + @override + void onPageStarted(String? url) => super.noSuchMethod( + Invocation.method( + #onPageStarted, + [url], + ), + returnValueForMissingStub: null, + ); + @override + void onPageFinished(String? url) => super.noSuchMethod( + Invocation.method( + #onPageFinished, + [url], + ), + returnValueForMissingStub: null, + ); + @override + void onProgress(int? progress) => super.noSuchMethod( + Invocation.method( + #onProgress, + [progress], + ), + returnValueForMissingStub: null, + ); + @override + void onWebResourceError(_i8.WebResourceError? error) => super.noSuchMethod( + Invocation.method( + #onWebResourceError, + [error], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [HttpRequestFactory]. @@ -1131,30 +2336,47 @@ class MockHttpRequestFactory extends _i1.Mock } @override - _i6.Future<_i2.HttpRequest> request(String? url, - {String? method, - bool? withCredentials, - String? responseType, - String? mimeType, - Map? requestHeaders, - dynamic sendData, - void Function(_i2.ProgressEvent)? onProgress}) => + _i6.Future<_i2.HttpRequest> request( + String? url, { + String? method, + bool? withCredentials, + String? responseType, + String? mimeType, + Map? requestHeaders, + dynamic sendData, + void Function(_i2.ProgressEvent)? onProgress, + }) => (super.noSuchMethod( - Invocation.method(#request, [ - url - ], { - #method: method, - #withCredentials: withCredentials, - #responseType: responseType, - #mimeType: mimeType, - #requestHeaders: requestHeaders, - #sendData: sendData, - #onProgress: onProgress - }), - returnValue: Future<_i2.HttpRequest>.value(_FakeHttpRequest_17())) - as _i6.Future<_i2.HttpRequest>); - @override - String toString() => super.toString(); + Invocation.method( + #request, + [url], + { + #method: method, + #withCredentials: withCredentials, + #responseType: responseType, + #mimeType: mimeType, + #requestHeaders: requestHeaders, + #sendData: sendData, + #onProgress: onProgress, + }, + ), + returnValue: _i6.Future<_i2.HttpRequest>.value(_FakeHttpRequest_17( + this, + Invocation.method( + #request, + [url], + { + #method: method, + #withCredentials: withCredentials, + #responseType: responseType, + #mimeType: mimeType, + #requestHeaders: requestHeaders, + #sendData: sendData, + #onProgress: onProgress, + }, + ), + )), + ) as _i6.Future<_i2.HttpRequest>); } /// A class which mocks [HttpRequest]. @@ -1166,122 +2388,216 @@ class MockHttpRequest extends _i1.Mock implements _i2.HttpRequest { } @override - Map get responseHeaders => - (super.noSuchMethod(Invocation.getter(#responseHeaders), - returnValue: {}) as Map); - @override - int get readyState => - (super.noSuchMethod(Invocation.getter(#readyState), returnValue: 0) - as int); - @override - String get responseType => - (super.noSuchMethod(Invocation.getter(#responseType), returnValue: '') - as String); - @override - set responseType(String? value) => - super.noSuchMethod(Invocation.setter(#responseType, value), - returnValueForMissingStub: null); - @override - set timeout(int? value) => - super.noSuchMethod(Invocation.setter(#timeout, value), - returnValueForMissingStub: null); - @override - _i2.HttpRequestUpload get upload => - (super.noSuchMethod(Invocation.getter(#upload), - returnValue: _FakeHttpRequestUpload_18()) as _i2.HttpRequestUpload); - @override - set withCredentials(bool? value) => - super.noSuchMethod(Invocation.setter(#withCredentials, value), - returnValueForMissingStub: null); - @override - _i6.Stream<_i2.Event> get onReadyStateChange => - (super.noSuchMethod(Invocation.getter(#onReadyStateChange), - returnValue: Stream<_i2.Event>.empty()) as _i6.Stream<_i2.Event>); - @override - _i6.Stream<_i2.ProgressEvent> get onAbort => - (super.noSuchMethod(Invocation.getter(#onAbort), - returnValue: Stream<_i2.ProgressEvent>.empty()) - as _i6.Stream<_i2.ProgressEvent>); - @override - _i6.Stream<_i2.ProgressEvent> get onError => - (super.noSuchMethod(Invocation.getter(#onError), - returnValue: Stream<_i2.ProgressEvent>.empty()) - as _i6.Stream<_i2.ProgressEvent>); - @override - _i6.Stream<_i2.ProgressEvent> get onLoad => - (super.noSuchMethod(Invocation.getter(#onLoad), - returnValue: Stream<_i2.ProgressEvent>.empty()) - as _i6.Stream<_i2.ProgressEvent>); - @override - _i6.Stream<_i2.ProgressEvent> get onLoadEnd => - (super.noSuchMethod(Invocation.getter(#onLoadEnd), - returnValue: Stream<_i2.ProgressEvent>.empty()) - as _i6.Stream<_i2.ProgressEvent>); - @override - _i6.Stream<_i2.ProgressEvent> get onLoadStart => - (super.noSuchMethod(Invocation.getter(#onLoadStart), - returnValue: Stream<_i2.ProgressEvent>.empty()) - as _i6.Stream<_i2.ProgressEvent>); - @override - _i6.Stream<_i2.ProgressEvent> get onProgress => - (super.noSuchMethod(Invocation.getter(#onProgress), - returnValue: Stream<_i2.ProgressEvent>.empty()) - as _i6.Stream<_i2.ProgressEvent>); - @override - _i6.Stream<_i2.ProgressEvent> get onTimeout => - (super.noSuchMethod(Invocation.getter(#onTimeout), - returnValue: Stream<_i2.ProgressEvent>.empty()) - as _i6.Stream<_i2.ProgressEvent>); - @override - _i2.Events get on => - (super.noSuchMethod(Invocation.getter(#on), returnValue: _FakeEvents_19()) - as _i2.Events); - @override - void open(String? method, String? url, - {bool? async, String? user, String? password}) => + Map get responseHeaders => (super.noSuchMethod( + Invocation.getter(#responseHeaders), + returnValue: {}, + ) as Map); + @override + int get readyState => (super.noSuchMethod( + Invocation.getter(#readyState), + returnValue: 0, + ) as int); + @override + String get responseType => (super.noSuchMethod( + Invocation.getter(#responseType), + returnValue: '', + ) as String); + @override + set responseType(String? value) => super.noSuchMethod( + Invocation.setter( + #responseType, + value, + ), + returnValueForMissingStub: null, + ); + @override + set timeout(int? value) => super.noSuchMethod( + Invocation.setter( + #timeout, + value, + ), + returnValueForMissingStub: null, + ); + @override + _i2.HttpRequestUpload get upload => (super.noSuchMethod( + Invocation.getter(#upload), + returnValue: _FakeHttpRequestUpload_18( + this, + Invocation.getter(#upload), + ), + ) as _i2.HttpRequestUpload); + @override + set withCredentials(bool? value) => super.noSuchMethod( + Invocation.setter( + #withCredentials, + value, + ), + returnValueForMissingStub: null, + ); + @override + _i6.Stream<_i2.Event> get onReadyStateChange => (super.noSuchMethod( + Invocation.getter(#onReadyStateChange), + returnValue: _i6.Stream<_i2.Event>.empty(), + ) as _i6.Stream<_i2.Event>); + @override + _i6.Stream<_i2.ProgressEvent> get onAbort => (super.noSuchMethod( + Invocation.getter(#onAbort), + returnValue: _i6.Stream<_i2.ProgressEvent>.empty(), + ) as _i6.Stream<_i2.ProgressEvent>); + @override + _i6.Stream<_i2.ProgressEvent> get onError => (super.noSuchMethod( + Invocation.getter(#onError), + returnValue: _i6.Stream<_i2.ProgressEvent>.empty(), + ) as _i6.Stream<_i2.ProgressEvent>); + @override + _i6.Stream<_i2.ProgressEvent> get onLoad => (super.noSuchMethod( + Invocation.getter(#onLoad), + returnValue: _i6.Stream<_i2.ProgressEvent>.empty(), + ) as _i6.Stream<_i2.ProgressEvent>); + @override + _i6.Stream<_i2.ProgressEvent> get onLoadEnd => (super.noSuchMethod( + Invocation.getter(#onLoadEnd), + returnValue: _i6.Stream<_i2.ProgressEvent>.empty(), + ) as _i6.Stream<_i2.ProgressEvent>); + @override + _i6.Stream<_i2.ProgressEvent> get onLoadStart => (super.noSuchMethod( + Invocation.getter(#onLoadStart), + returnValue: _i6.Stream<_i2.ProgressEvent>.empty(), + ) as _i6.Stream<_i2.ProgressEvent>); + @override + _i6.Stream<_i2.ProgressEvent> get onProgress => (super.noSuchMethod( + Invocation.getter(#onProgress), + returnValue: _i6.Stream<_i2.ProgressEvent>.empty(), + ) as _i6.Stream<_i2.ProgressEvent>); + @override + _i6.Stream<_i2.ProgressEvent> get onTimeout => (super.noSuchMethod( + Invocation.getter(#onTimeout), + returnValue: _i6.Stream<_i2.ProgressEvent>.empty(), + ) as _i6.Stream<_i2.ProgressEvent>); + @override + _i2.Events get on => (super.noSuchMethod( + Invocation.getter(#on), + returnValue: _FakeEvents_19( + this, + Invocation.getter(#on), + ), + ) as _i2.Events); + @override + void open( + String? method, + String? url, { + bool? async, + String? user, + String? password, + }) => super.noSuchMethod( - Invocation.method(#open, [method, url], - {#async: async, #user: user, #password: password}), - returnValueForMissingStub: null); - @override - void abort() => super.noSuchMethod(Invocation.method(#abort, []), - returnValueForMissingStub: null); - @override - String getAllResponseHeaders() => - (super.noSuchMethod(Invocation.method(#getAllResponseHeaders, []), - returnValue: '') as String); + Invocation.method( + #open, + [ + method, + url, + ], + { + #async: async, + #user: user, + #password: password, + }, + ), + returnValueForMissingStub: null, + ); + @override + void abort() => super.noSuchMethod( + Invocation.method( + #abort, + [], + ), + returnValueForMissingStub: null, + ); + @override + String getAllResponseHeaders() => (super.noSuchMethod( + Invocation.method( + #getAllResponseHeaders, + [], + ), + returnValue: '', + ) as String); @override String? getResponseHeader(String? name) => - (super.noSuchMethod(Invocation.method(#getResponseHeader, [name])) - as String?); - @override - void overrideMimeType(String? mime) => - super.noSuchMethod(Invocation.method(#overrideMimeType, [mime]), - returnValueForMissingStub: null); - @override - void send([dynamic body_OR_data]) => - super.noSuchMethod(Invocation.method(#send, [body_OR_data]), - returnValueForMissingStub: null); - @override - void setRequestHeader(String? name, String? value) => - super.noSuchMethod(Invocation.method(#setRequestHeader, [name, value]), - returnValueForMissingStub: null); - @override - void addEventListener(String? type, _i2.EventListener? listener, - [bool? useCapture]) => + (super.noSuchMethod(Invocation.method( + #getResponseHeader, + [name], + )) as String?); + @override + void overrideMimeType(String? mime) => super.noSuchMethod( + Invocation.method( + #overrideMimeType, + [mime], + ), + returnValueForMissingStub: null, + ); + @override + void send([dynamic body_OR_data]) => super.noSuchMethod( + Invocation.method( + #send, + [body_OR_data], + ), + returnValueForMissingStub: null, + ); + @override + void setRequestHeader( + String? name, + String? value, + ) => super.noSuchMethod( - Invocation.method(#addEventListener, [type, listener, useCapture]), - returnValueForMissingStub: null); - @override - void removeEventListener(String? type, _i2.EventListener? listener, - [bool? useCapture]) => + Invocation.method( + #setRequestHeader, + [ + name, + value, + ], + ), + returnValueForMissingStub: null, + ); + @override + void addEventListener( + String? type, + _i2.EventListener? listener, [ + bool? useCapture, + ]) => super.noSuchMethod( - Invocation.method(#removeEventListener, [type, listener, useCapture]), - returnValueForMissingStub: null); - @override - bool dispatchEvent(_i2.Event? event) => - (super.noSuchMethod(Invocation.method(#dispatchEvent, [event]), - returnValue: false) as bool); - @override - String toString() => super.toString(); + Invocation.method( + #addEventListener, + [ + type, + listener, + useCapture, + ], + ), + returnValueForMissingStub: null, + ); + @override + void removeEventListener( + String? type, + _i2.EventListener? listener, [ + bool? useCapture, + ]) => + super.noSuchMethod( + Invocation.method( + #removeEventListener, + [ + type, + listener, + useCapture, + ], + ), + returnValueForMissingStub: null, + ); + @override + bool dispatchEvent(_i2.Event? event) => (super.noSuchMethod( + Invocation.method( + #dispatchEvent, + [event], + ), + returnValue: false, + ) as bool); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index c9bfca1e5e9e..4247f0c40b22 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -12,7 +12,62 @@ ## NEXT +* Updates code for `no_leading_underscores_for_local_identifiers` lint. + +## 2.9.5 + +* Updates imports for `prefer_relative_imports`. + +## 2.9.4 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. +* Fixes typo in an internal method name, from `setCookieForInsances` to `setCookieForInstances`. + +## 2.9.3 + +* Updates `webview_flutter_platform_interface` constraint to the correct minimum + version. + +## 2.9.2 + +* Fixes crash when an Objective-C object in `FWFInstanceManager` is released, but the dealloc + callback is no longer available. + +## 2.9.1 + +* Fixes regression where the behavior for the `UIScrollView` insets were removed. + +## 2.9.0 + +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). +* Replaces platform implementation with WebKit API built with pigeon. + +## 2.8.1 + +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/104231). + +## 2.8.0 + +* Raises minimum Dart version to 2.17 and Flutter version to 3.0.0. + +## 2.7.5 + +* Minor fixes for new analysis options. + +## 2.7.4 + +* Removes unnecessary imports. +* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors + lint warnings. + +## 2.7.3 + +* Removes two occurrences of the compiler warning: "'RequiresUserActionForMediaPlayback' is deprecated: first deprecated in ios 10.0". + +## 2.7.2 + * Fixes an integration test race condition. +* Migrates deprecated `Scaffold.showSnackBar` to `ScaffoldMessenger` in example app. ## 2.7.1 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m new file mode 100644 index 000000000000..ca7d6f938599 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m @@ -0,0 +1,117 @@ +// 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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFDataConvertersTests : XCTestCase +@end + +@implementation FWFDataConvertersTests +- (void)testFWFNSURLRequestFromRequestData { + NSURLRequest *request = FWFNSURLRequestFromRequestData([FWFNSUrlRequestData + makeWithUrl:@"https://flutter.dev" + httpMethod:@"post" + httpBody:[FlutterStandardTypedData typedDataWithBytes:[NSData data]] + allHttpHeaderFields:@{@"a" : @"header"}]); + + XCTAssertEqualObjects(request.URL, [NSURL URLWithString:@"https://flutter.dev"]); + XCTAssertEqualObjects(request.HTTPMethod, @"POST"); + XCTAssertEqualObjects(request.HTTPBody, [NSData data]); + XCTAssertEqualObjects(request.allHTTPHeaderFields, @{@"a" : @"header"}); +} + +- (void)testFWFNSURLRequestFromRequestDataDoesNotOverrideDefaultValuesWithNull { + NSURLRequest *request = + FWFNSURLRequestFromRequestData([FWFNSUrlRequestData makeWithUrl:@"https://flutter.dev" + httpMethod:nil + httpBody:nil + allHttpHeaderFields:@{}]); + + XCTAssertEqualObjects(request.HTTPMethod, @"GET"); +} + +- (void)testFWFNSHTTPCookieFromCookieData { + NSHTTPCookie *cookie = FWFNSHTTPCookieFromCookieData([FWFNSHttpCookieData + makeWithPropertyKeys:@[ [FWFNSHttpCookiePropertyKeyEnumData + makeWithValue:FWFNSHttpCookiePropertyKeyEnumName] ] + propertyValues:@[ @"cookieName" ]]); + XCTAssertEqualObjects(cookie, + [NSHTTPCookie cookieWithProperties:@{NSHTTPCookieName : @"cookieName"}]); +} + +- (void)testFWFWKUserScriptFromScriptData { + WKUserScript *userScript = FWFWKUserScriptFromScriptData([FWFWKUserScriptData + makeWithSource:@"mySource" + injectionTime:[FWFWKUserScriptInjectionTimeEnumData + makeWithValue:FWFWKUserScriptInjectionTimeEnumAtDocumentStart] + isMainFrameOnly:@NO]); + + XCTAssertEqualObjects(userScript.source, @"mySource"); + XCTAssertEqual(userScript.injectionTime, WKUserScriptInjectionTimeAtDocumentStart); + XCTAssertEqual(userScript.isForMainFrameOnly, NO); +} + +- (void)testFWFWKNavigationActionDataFromNavigationAction { + WKNavigationAction *mockNavigationAction = OCMClassMock([WKNavigationAction class]); + + NSURLRequest *request = + [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.flutter.dev/"]]; + OCMStub([mockNavigationAction request]).andReturn(request); + + WKFrameInfo *mockFrameInfo = OCMClassMock([WKFrameInfo class]); + OCMStub([mockFrameInfo isMainFrame]).andReturn(YES); + OCMStub([mockNavigationAction targetFrame]).andReturn(mockFrameInfo); + + FWFWKNavigationActionData *data = + FWFWKNavigationActionDataFromNavigationAction(mockNavigationAction); + XCTAssertNotNil(data); +} + +- (void)testFWFNSUrlRequestDataFromNSURLRequest { + NSMutableURLRequest *request = + [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://www.flutter.dev/"]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [@"aString" dataUsingEncoding:NSUTF8StringEncoding]; + request.allHTTPHeaderFields = @{@"a" : @"field"}; + + FWFNSUrlRequestData *data = FWFNSUrlRequestDataFromNSURLRequest(request); + XCTAssertEqualObjects(data.url, @"https://www.flutter.dev/"); + XCTAssertEqualObjects(data.httpMethod, @"POST"); + XCTAssertEqualObjects(data.httpBody.data, [@"aString" dataUsingEncoding:NSUTF8StringEncoding]); + XCTAssertEqualObjects(data.allHttpHeaderFields, @{@"a" : @"field"}); +} + +- (void)testFWFWKFrameInfoDataFromWKFrameInfo { + WKFrameInfo *mockFrameInfo = OCMClassMock([WKFrameInfo class]); + OCMStub([mockFrameInfo isMainFrame]).andReturn(YES); + + FWFWKFrameInfoData *targetFrameData = FWFWKFrameInfoDataFromWKFrameInfo(mockFrameInfo); + XCTAssertEqualObjects(targetFrameData.isMainFrame, @YES); +} + +- (void)testFWFNSErrorDataFromNSError { + NSError *error = [NSError errorWithDomain:@"domain" + code:23 + userInfo:@{NSLocalizedDescriptionKey : @"description"}]; + + FWFNSErrorData *data = FWFNSErrorDataFromNSError(error); + XCTAssertEqualObjects(data.code, @23); + XCTAssertEqualObjects(data.domain, @"domain"); + XCTAssertEqualObjects(data.localizedDescription, @"description"); +} + +- (void)testFWFWKScriptMessageDataFromWKScriptMessage { + WKScriptMessage *mockScriptMessage = OCMClassMock([WKScriptMessage class]); + OCMStub([mockScriptMessage name]).andReturn(@"name"); + OCMStub([mockScriptMessage body]).andReturn(@"message"); + + FWFWKScriptMessageData *data = FWFWKScriptMessageDataFromWKScriptMessage(mockScriptMessage); + XCTAssertEqualObjects(data.name, @"name"); + XCTAssertEqualObjects(data.body, @"message"); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFHTTPCookieStoreHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFHTTPCookieStoreHostApiTests.m new file mode 100644 index 000000000000..45eefc3897ec --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFHTTPCookieStoreHostApiTests.m @@ -0,0 +1,55 @@ +// 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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFHTTPCookieStoreHostApiTests : XCTestCase +@end + +@implementation FWFHTTPCookieStoreHostApiTests +- (void)testCreateFromWebsiteDataStoreWithIdentifier API_AVAILABLE(ios(11.0)) { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFHTTPCookieStoreHostApiImpl *hostAPI = + [[FWFHTTPCookieStoreHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + WKWebsiteDataStore *mockDataStore = OCMClassMock([WKWebsiteDataStore class]); + OCMStub([mockDataStore httpCookieStore]).andReturn(OCMClassMock([WKHTTPCookieStore class])); + [instanceManager addDartCreatedInstance:mockDataStore withIdentifier:0]; + + FlutterError *error; + [hostAPI createFromWebsiteDataStoreWithIdentifier:@1 dataStoreIdentifier:@0 error:&error]; + WKHTTPCookieStore *cookieStore = (WKHTTPCookieStore *)[instanceManager instanceForIdentifier:1]; + XCTAssertTrue([cookieStore isKindOfClass:[WKHTTPCookieStore class]]); + XCTAssertNil(error); +} + +- (void)testSetCookie API_AVAILABLE(ios(11.0)) { + WKHTTPCookieStore *mockHttpCookieStore = OCMClassMock([WKHTTPCookieStore class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockHttpCookieStore withIdentifier:0]; + + FWFHTTPCookieStoreHostApiImpl *hostAPI = + [[FWFHTTPCookieStoreHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FWFNSHttpCookieData *cookieData = [FWFNSHttpCookieData + makeWithPropertyKeys:@[ [FWFNSHttpCookiePropertyKeyEnumData + makeWithValue:FWFNSHttpCookiePropertyKeyEnumName] ] + propertyValues:@[ @"hello" ]]; + FlutterError *__block blockError; + [hostAPI setCookieForStoreWithIdentifier:@0 + cookie:cookieData + completion:^(FlutterError *error) { + blockError = error; + }]; + OCMVerify([mockHttpCookieStore + setCookie:[NSHTTPCookie cookieWithProperties:@{NSHTTPCookieName : @"hello"}] + completionHandler:OCMOCK_ANY]); + XCTAssertNil(blockError); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFInstanceManagerTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFInstanceManagerTests.m new file mode 100644 index 000000000000..c893ab51ef42 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFInstanceManagerTests.m @@ -0,0 +1,55 @@ +// 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 webview_flutter_wkwebview; +@import webview_flutter_wkwebview.Test; + +@interface FWFInstanceManagerTests : XCTestCase +@end + +@implementation FWFInstanceManagerTests +- (void)testAddDartCreatedInstance { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + NSObject *object = [[NSObject alloc] init]; + + [instanceManager addDartCreatedInstance:object withIdentifier:0]; + XCTAssertEqualObjects([instanceManager instanceForIdentifier:0], object); + XCTAssertEqual([instanceManager identifierWithStrongReferenceForInstance:object], 0); +} + +- (void)testAddHostCreatedInstance { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + NSObject *object = [[NSObject alloc] init]; + [instanceManager addHostCreatedInstance:object]; + + long identifier = [instanceManager identifierWithStrongReferenceForInstance:object]; + XCTAssertNotEqual(identifier, NSNotFound); + XCTAssertEqualObjects([instanceManager instanceForIdentifier:identifier], object); +} + +- (void)testRemoveInstanceWithIdentifier { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + NSObject *object = [[NSObject alloc] init]; + + [instanceManager addDartCreatedInstance:object withIdentifier:0]; + + XCTAssertEqualObjects([instanceManager removeInstanceWithIdentifier:0], object); + XCTAssertEqual([instanceManager strongInstanceCount], 0); +} + +- (void)testDeallocCallbackIsIgnoredIfNull { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + // This sets deallocCallback to nil to test that uses are null checked. + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] initWithDeallocCallback:nil]; +#pragma clang diagnostic pop + + [instanceManager addDartCreatedInstance:[[NSObject alloc] init] withIdentifier:0]; + + // Tests that this doesn't cause a EXC_BAD_ACCESS crash. + [instanceManager removeInstanceWithIdentifier:0]; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFNavigationDelegateHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFNavigationDelegateHostApiTests.m new file mode 100644 index 000000000000..570a1f73ad9b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFNavigationDelegateHostApiTests.m @@ -0,0 +1,216 @@ +// 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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFNavigationDelegateHostApiTests : XCTestCase +@end + +@implementation FWFNavigationDelegateHostApiTests +/** + * Creates a partially mocked FWFNavigationDelegate and adds it to instanceManager. + * + * @param instanceManager Instance manager to add the delegate to. + * @param identifier Identifier for the delegate added to the instanceManager. + * + * @return A mock FWFNavigationDelegate. + */ +- (id)mockNavigationDelegateWithManager:(FWFInstanceManager *)instanceManager + identifier:(long)identifier { + FWFNavigationDelegate *navigationDelegate = [[FWFNavigationDelegate alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + [instanceManager addDartCreatedInstance:navigationDelegate withIdentifier:0]; + return OCMPartialMock(navigationDelegate); +} + +/** + * Creates a mock FWFNavigationDelegateFlutterApiImpl with instanceManager. + * + * @param instanceManager Instance manager passed to the Flutter API. + * + * @return A mock FWFNavigationDelegateFlutterApiImpl. + */ +- (id)mockFlutterApiWithManager:(FWFInstanceManager *)instanceManager { + FWFNavigationDelegateFlutterApiImpl *flutterAPI = [[FWFNavigationDelegateFlutterApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + return OCMPartialMock(flutterAPI); +} + +- (void)testCreateWithIdentifier { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFNavigationDelegateHostApiImpl *hostAPI = [[FWFNavigationDelegateHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI createWithIdentifier:@0 error:&error]; + FWFNavigationDelegate *navigationDelegate = + (FWFNavigationDelegate *)[instanceManager instanceForIdentifier:0]; + + XCTAssertTrue([navigationDelegate conformsToProtocol:@protocol(WKNavigationDelegate)]); + XCTAssertNil(error); +} + +- (void)testDidFinishNavigation { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + + FWFNavigationDelegate *mockDelegate = [self mockNavigationDelegateWithManager:instanceManager + identifier:0]; + FWFNavigationDelegateFlutterApiImpl *mockFlutterAPI = + [self mockFlutterApiWithManager:instanceManager]; + + OCMStub([mockDelegate navigationDelegateAPI]).andReturn(mockFlutterAPI); + + WKWebView *mockWebView = OCMClassMock([WKWebView class]); + OCMStub([mockWebView URL]).andReturn([NSURL URLWithString:@"https://flutter.dev/"]); + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:1]; + + [mockDelegate webView:mockWebView didFinishNavigation:OCMClassMock([WKNavigation class])]; + OCMVerify([mockFlutterAPI didFinishNavigationForDelegateWithIdentifier:@0 + webViewIdentifier:@1 + URL:@"https://flutter.dev/" + completion:OCMOCK_ANY]); +} + +- (void)testDidStartProvisionalNavigation { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + + FWFNavigationDelegate *mockDelegate = [self mockNavigationDelegateWithManager:instanceManager + identifier:0]; + FWFNavigationDelegateFlutterApiImpl *mockFlutterAPI = + [self mockFlutterApiWithManager:instanceManager]; + + OCMStub([mockDelegate navigationDelegateAPI]).andReturn(mockFlutterAPI); + + WKWebView *mockWebView = OCMClassMock([WKWebView class]); + OCMStub([mockWebView URL]).andReturn([NSURL URLWithString:@"https://flutter.dev/"]); + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:1]; + + [mockDelegate webView:mockWebView + didStartProvisionalNavigation:OCMClassMock([WKNavigation class])]; + OCMVerify([mockFlutterAPI + didStartProvisionalNavigationForDelegateWithIdentifier:@0 + webViewIdentifier:@1 + URL:@"https://flutter.dev/" + completion:OCMOCK_ANY]); +} + +- (void)testDecidePolicyForNavigationAction { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + + FWFNavigationDelegate *mockDelegate = [self mockNavigationDelegateWithManager:instanceManager + identifier:0]; + FWFNavigationDelegateFlutterApiImpl *mockFlutterAPI = + [self mockFlutterApiWithManager:instanceManager]; + + OCMStub([mockDelegate navigationDelegateAPI]).andReturn(mockFlutterAPI); + + WKWebView *mockWebView = OCMClassMock([WKWebView class]); + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:1]; + + WKNavigationAction *mockNavigationAction = OCMClassMock([WKNavigationAction class]); + OCMStub([mockNavigationAction request]) + .andReturn([NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.flutter.dev"]]); + + WKFrameInfo *mockFrameInfo = OCMClassMock([WKFrameInfo class]); + OCMStub([mockFrameInfo isMainFrame]).andReturn(YES); + OCMStub([mockNavigationAction targetFrame]).andReturn(mockFrameInfo); + + OCMStub([mockFlutterAPI + decidePolicyForNavigationActionForDelegateWithIdentifier:@0 + webViewIdentifier:@1 + navigationAction: + [OCMArg isKindOfClass:[FWFWKNavigationActionData + class]] + completion: + ([OCMArg + invokeBlockWithArgs: + [FWFWKNavigationActionPolicyEnumData + makeWithValue: + FWFWKNavigationActionPolicyEnumCancel], + [NSNull null], nil])]); + + WKNavigationActionPolicy __block callbackPolicy = -1; + [mockDelegate webView:mockWebView + decidePolicyForNavigationAction:mockNavigationAction + decisionHandler:^(WKNavigationActionPolicy policy) { + callbackPolicy = policy; + }]; + XCTAssertEqual(callbackPolicy, WKNavigationActionPolicyCancel); +} + +- (void)testDidFailNavigation { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + + FWFNavigationDelegate *mockDelegate = [self mockNavigationDelegateWithManager:instanceManager + identifier:0]; + FWFNavigationDelegateFlutterApiImpl *mockFlutterAPI = + [self mockFlutterApiWithManager:instanceManager]; + + OCMStub([mockDelegate navigationDelegateAPI]).andReturn(mockFlutterAPI); + + WKWebView *mockWebView = OCMClassMock([WKWebView class]); + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:1]; + + [mockDelegate webView:mockWebView + didFailNavigation:OCMClassMock([WKNavigation class]) + withError:[NSError errorWithDomain:@"domain" code:0 userInfo:nil]]; + OCMVerify([mockFlutterAPI + didFailNavigationForDelegateWithIdentifier:@0 + webViewIdentifier:@1 + error:[OCMArg isKindOfClass:[FWFNSErrorData class]] + completion:OCMOCK_ANY]); +} + +- (void)testDidFailProvisionalNavigation { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + + FWFNavigationDelegate *mockDelegate = [self mockNavigationDelegateWithManager:instanceManager + identifier:0]; + FWFNavigationDelegateFlutterApiImpl *mockFlutterAPI = + [self mockFlutterApiWithManager:instanceManager]; + + OCMStub([mockDelegate navigationDelegateAPI]).andReturn(mockFlutterAPI); + + WKWebView *mockWebView = OCMClassMock([WKWebView class]); + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:1]; + + [mockDelegate webView:mockWebView + didFailProvisionalNavigation:OCMClassMock([WKNavigation class]) + withError:[NSError errorWithDomain:@"domain" code:0 userInfo:nil]]; + OCMVerify([mockFlutterAPI + didFailProvisionalNavigationForDelegateWithIdentifier:@0 + webViewIdentifier:@1 + error:[OCMArg isKindOfClass:[FWFNSErrorData + class]] + completion:OCMOCK_ANY]); +} + +- (void)testWebViewWebContentProcessDidTerminate { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + + FWFNavigationDelegate *mockDelegate = [self mockNavigationDelegateWithManager:instanceManager + identifier:0]; + FWFNavigationDelegateFlutterApiImpl *mockFlutterAPI = + [self mockFlutterApiWithManager:instanceManager]; + + OCMStub([mockDelegate navigationDelegateAPI]).andReturn(mockFlutterAPI); + + WKWebView *mockWebView = OCMClassMock([WKWebView class]); + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:1]; + + [mockDelegate webViewWebContentProcessDidTerminate:mockWebView]; + OCMVerify([mockFlutterAPI + webViewWebContentProcessDidTerminateForDelegateWithIdentifier:@0 + webViewIdentifier:@1 + completion:OCMOCK_ANY]); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFObjectHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFObjectHostApiTests.m new file mode 100644 index 000000000000..b8e41d142331 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFObjectHostApiTests.m @@ -0,0 +1,146 @@ +// 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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFObjectHostApiTests : XCTestCase +@end + +@implementation FWFObjectHostApiTests +/** + * Creates a partially mocked FWFObject and adds it to instanceManager. + * + * @param instanceManager Instance manager to add the delegate to. + * @param identifier Identifier for the delegate added to the instanceManager. + * + * @return A mock FWFObject. + */ +- (id)mockObjectWithManager:(FWFInstanceManager *)instanceManager identifier:(long)identifier { + FWFObject *object = + [[FWFObject alloc] initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + [instanceManager addDartCreatedInstance:object withIdentifier:0]; + return OCMPartialMock(object); +} + +/** + * Creates a mock FWFObjectFlutterApiImpl with instanceManager. + * + * @param instanceManager Instance manager passed to the Flutter API. + * + * @return A mock FWFObjectFlutterApiImpl. + */ +- (id)mockFlutterApiWithManager:(FWFInstanceManager *)instanceManager { + FWFObjectFlutterApiImpl *flutterAPI = [[FWFObjectFlutterApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + return OCMPartialMock(flutterAPI); +} + +- (void)testAddObserver { + NSObject *mockObject = OCMClassMock([NSObject class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockObject withIdentifier:0]; + + FWFObjectHostApiImpl *hostAPI = + [[FWFObjectHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + NSObject *observerObject = [[NSObject alloc] init]; + [instanceManager addDartCreatedInstance:observerObject withIdentifier:1]; + + FlutterError *error; + [hostAPI + addObserverForObjectWithIdentifier:@0 + observerIdentifier:@1 + keyPath:@"myKey" + options:@[ + [FWFNSKeyValueObservingOptionsEnumData + makeWithValue:FWFNSKeyValueObservingOptionsEnumOldValue], + [FWFNSKeyValueObservingOptionsEnumData + makeWithValue:FWFNSKeyValueObservingOptionsEnumNewValue] + ] + error:&error]; + + OCMVerify([mockObject addObserver:observerObject + forKeyPath:@"myKey" + options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) + context:nil]); + XCTAssertNil(error); +} + +- (void)testRemoveObserver { + NSObject *mockObject = OCMClassMock([NSObject class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockObject withIdentifier:0]; + + FWFObjectHostApiImpl *hostAPI = + [[FWFObjectHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + NSObject *observerObject = [[NSObject alloc] init]; + [instanceManager addDartCreatedInstance:observerObject withIdentifier:1]; + + FlutterError *error; + [hostAPI removeObserverForObjectWithIdentifier:@0 + observerIdentifier:@1 + keyPath:@"myKey" + error:&error]; + OCMVerify([mockObject removeObserver:observerObject forKeyPath:@"myKey"]); + XCTAssertNil(error); +} + +- (void)testDispose { + NSObject *object = [[NSObject alloc] init]; + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:object withIdentifier:0]; + + FWFObjectHostApiImpl *hostAPI = + [[FWFObjectHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostAPI disposeObjectWithIdentifier:@0 error:&error]; + // Only the strong reference is removed, so the weak reference will remain until object is set to + // nil. + object = nil; + XCTAssertFalse([instanceManager containsInstance:object]); + XCTAssertNil(error); +} + +- (void)testObserveValueForKeyPath { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + + FWFObject *mockObject = [self mockObjectWithManager:instanceManager identifier:0]; + FWFObjectFlutterApiImpl *mockFlutterAPI = [self mockFlutterApiWithManager:instanceManager]; + + OCMStub([mockObject objectApi]).andReturn(mockFlutterAPI); + + NSObject *object = [[NSObject alloc] init]; + [instanceManager addDartCreatedInstance:object withIdentifier:1]; + + [mockObject observeValueForKeyPath:@"keyPath" + ofObject:object + change:@{NSKeyValueChangeOldKey : @"key"} + context:nil]; + OCMVerify([mockFlutterAPI + observeValueForObjectWithIdentifier:@0 + keyPath:@"keyPath" + objectIdentifier:@1 + changeKeys:[OCMArg checkWithBlock:^BOOL( + NSArray + *value) { + return value[0].value == FWFNSKeyValueChangeKeyEnumOldValue; + }] + changeValues:[OCMArg checkWithBlock:^BOOL(id value) { + return [@"key" isEqual:value[0]]; + }] + completion:OCMOCK_ANY]); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFPreferencesHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFPreferencesHostApiTests.m new file mode 100644 index 000000000000..95b81ad5c389 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFPreferencesHostApiTests.m @@ -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. + +@import Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFPreferencesHostApiTests : XCTestCase +@end + +@implementation FWFPreferencesHostApiTests +- (void)testCreateFromWebViewConfigurationWithIdentifier { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFPreferencesHostApiImpl *hostAPI = + [[FWFPreferencesHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + [instanceManager addDartCreatedInstance:[[WKWebViewConfiguration alloc] init] withIdentifier:0]; + + FlutterError *error; + [hostAPI createFromWebViewConfigurationWithIdentifier:@1 configurationIdentifier:@0 error:&error]; + WKPreferences *preferences = (WKPreferences *)[instanceManager instanceForIdentifier:1]; + XCTAssertTrue([preferences isKindOfClass:[WKPreferences class]]); + XCTAssertNil(error); +} + +- (void)testSetJavaScriptEnabled { + WKPreferences *mockPreferences = OCMClassMock([WKPreferences class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockPreferences withIdentifier:0]; + + FWFPreferencesHostApiImpl *hostAPI = + [[FWFPreferencesHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostAPI setJavaScriptEnabledForPreferencesWithIdentifier:@0 isEnabled:@YES error:&error]; + OCMVerify([mockPreferences setJavaScriptEnabled:YES]); + XCTAssertNil(error); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFScriptMessageHandlerHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFScriptMessageHandlerHostApiTests.m new file mode 100644 index 000000000000..84d31d1c543e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFScriptMessageHandlerHostApiTests.m @@ -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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFScriptMessageHandlerHostApiTests : XCTestCase +@end + +@implementation FWFScriptMessageHandlerHostApiTests +/** + * Creates a partially mocked FWFScriptMessageHandler and adds it to instanceManager. + * + * @param instanceManager Instance manager to add the delegate to. + * @param identifier Identifier for the delegate added to the instanceManager. + * + * @return A mock FWFScriptMessageHandler. + */ +- (id)mockHandlerWithManager:(FWFInstanceManager *)instanceManager identifier:(long)identifier { + FWFScriptMessageHandler *handler = [[FWFScriptMessageHandler alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + [instanceManager addDartCreatedInstance:handler withIdentifier:0]; + return OCMPartialMock(handler); +} + +/** + * Creates a mock FWFScriptMessageHandlerFlutterApiImpl with instanceManager. + * + * @param instanceManager Instance manager passed to the Flutter API. + * + * @return A mock FWFScriptMessageHandlerFlutterApiImpl. + */ +- (id)mockFlutterApiWithManager:(FWFInstanceManager *)instanceManager { + FWFScriptMessageHandlerFlutterApiImpl *flutterAPI = [[FWFScriptMessageHandlerFlutterApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + return OCMPartialMock(flutterAPI); +} + +- (void)testCreateWithIdentifier { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFScriptMessageHandlerHostApiImpl *hostAPI = [[FWFScriptMessageHandlerHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI createWithIdentifier:@0 error:&error]; + + FWFScriptMessageHandler *scriptMessageHandler = + (FWFScriptMessageHandler *)[instanceManager instanceForIdentifier:0]; + + XCTAssertTrue([scriptMessageHandler conformsToProtocol:@protocol(WKScriptMessageHandler)]); + XCTAssertNil(error); +} + +- (void)testDidReceiveScriptMessageForHandler { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + + FWFScriptMessageHandler *mockHandler = [self mockHandlerWithManager:instanceManager identifier:0]; + FWFScriptMessageHandlerFlutterApiImpl *mockFlutterAPI = + [self mockFlutterApiWithManager:instanceManager]; + + OCMStub([mockHandler scriptMessageHandlerAPI]).andReturn(mockFlutterAPI); + + WKUserContentController *userContentController = [[WKUserContentController alloc] init]; + [instanceManager addDartCreatedInstance:userContentController withIdentifier:1]; + + WKScriptMessage *mockScriptMessage = OCMClassMock([WKScriptMessage class]); + OCMStub([mockScriptMessage name]).andReturn(@"name"); + OCMStub([mockScriptMessage body]).andReturn(@"message"); + + [mockHandler userContentController:userContentController + didReceiveScriptMessage:mockScriptMessage]; + OCMVerify([mockFlutterAPI + didReceiveScriptMessageForHandlerWithIdentifier:@0 + userContentControllerIdentifier:@1 + message:[OCMArg isKindOfClass:[FWFWKScriptMessageData + class]] + completion:OCMOCK_ANY]); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFScrollViewHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFScrollViewHostApiTests.m new file mode 100644 index 000000000000..ede8dcf35d89 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFScrollViewHostApiTests.m @@ -0,0 +1,64 @@ +// 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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFScrollViewHostApiTests : XCTestCase +@end + +@implementation FWFScrollViewHostApiTests +- (void)testGetContentOffset { + UIScrollView *mockScrollView = OCMClassMock([UIScrollView class]); + OCMStub([mockScrollView contentOffset]).andReturn(CGPointMake(1.0, 2.0)); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockScrollView withIdentifier:0]; + + FWFScrollViewHostApiImpl *hostAPI = + [[FWFScrollViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + NSArray *expectedValue = @[ @1.0, @2.0 ]; + XCTAssertEqualObjects([hostAPI contentOffsetForScrollViewWithIdentifier:@0 error:&error], + expectedValue); + XCTAssertNil(error); +} + +- (void)testScrollBy { + UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)]; + scrollView.contentOffset = CGPointMake(1, 2); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:scrollView withIdentifier:0]; + + FWFScrollViewHostApiImpl *hostAPI = + [[FWFScrollViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostAPI scrollByForScrollViewWithIdentifier:@0 x:@1 y:@2 error:&error]; + XCTAssertEqual(scrollView.contentOffset.x, 2); + XCTAssertEqual(scrollView.contentOffset.y, 4); + XCTAssertNil(error); +} + +- (void)testSetContentOffset { + UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)]; + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:scrollView withIdentifier:0]; + + FWFScrollViewHostApiImpl *hostAPI = + [[FWFScrollViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostAPI setContentOffsetForScrollViewWithIdentifier:@0 toX:@1 y:@2 error:&error]; + XCTAssertEqual(scrollView.contentOffset.x, 1); + XCTAssertEqual(scrollView.contentOffset.y, 2); + XCTAssertNil(error); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIDelegateHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIDelegateHostApiTests.m new file mode 100644 index 000000000000..939c14873fa4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIDelegateHostApiTests.m @@ -0,0 +1,100 @@ +// 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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFUIDelegateHostApiTests : XCTestCase +@end + +@implementation FWFUIDelegateHostApiTests +/** + * Creates a partially mocked FWFUIDelegate and adds it to instanceManager. + * + * @param instanceManager Instance manager to add the delegate to. + * @param identifier Identifier for the delegate added to the instanceManager. + * + * @return A mock FWFUIDelegate. + */ +- (id)mockDelegateWithManager:(FWFInstanceManager *)instanceManager identifier:(long)identifier { + FWFUIDelegate *delegate = [[FWFUIDelegate alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + [instanceManager addDartCreatedInstance:delegate withIdentifier:0]; + return OCMPartialMock(delegate); +} + +/** + * Creates a mock FWFUIDelegateFlutterApiImpl with instanceManager. + * + * @param instanceManager Instance manager passed to the Flutter API. + * + * @return A mock FWFUIDelegateFlutterApiImpl. + */ +- (id)mockFlutterApiWithManager:(FWFInstanceManager *)instanceManager { + FWFUIDelegateFlutterApiImpl *flutterAPI = [[FWFUIDelegateFlutterApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + return OCMPartialMock(flutterAPI); +} + +- (void)testCreateWithIdentifier { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFUIDelegateHostApiImpl *hostAPI = [[FWFUIDelegateHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI createWithIdentifier:@0 error:&error]; + FWFUIDelegate *delegate = (FWFUIDelegate *)[instanceManager instanceForIdentifier:0]; + + XCTAssertTrue([delegate conformsToProtocol:@protocol(WKUIDelegate)]); + XCTAssertNil(error); +} + +- (void)testOnCreateWebViewForDelegateWithIdentifier { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + + FWFUIDelegate *mockDelegate = [self mockDelegateWithManager:instanceManager identifier:0]; + FWFUIDelegateFlutterApiImpl *mockFlutterAPI = [self mockFlutterApiWithManager:instanceManager]; + + OCMStub([mockDelegate UIDelegateAPI]).andReturn(mockFlutterAPI); + + WKWebView *mockWebView = OCMClassMock([WKWebView class]); + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:1]; + + WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; + id mockConfigurationFlutterApi = OCMPartialMock(mockFlutterAPI.webViewConfigurationFlutterApi); + NSNumber *__block configurationIdentifier; + OCMStub([mockConfigurationFlutterApi createWithIdentifier:[OCMArg checkWithBlock:^BOOL(id value) { + configurationIdentifier = value; + return YES; + }] + completion:OCMOCK_ANY]); + + WKNavigationAction *mockNavigationAction = OCMClassMock([WKNavigationAction class]); + OCMStub([mockNavigationAction request]) + .andReturn([NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.flutter.dev"]]); + + WKFrameInfo *mockFrameInfo = OCMClassMock([WKFrameInfo class]); + OCMStub([mockFrameInfo isMainFrame]).andReturn(YES); + OCMStub([mockNavigationAction targetFrame]).andReturn(mockFrameInfo); + + [mockDelegate webView:mockWebView + createWebViewWithConfiguration:configuration + forNavigationAction:mockNavigationAction + windowFeatures:OCMClassMock([WKWindowFeatures class])]; + OCMVerify([mockFlutterAPI + onCreateWebViewForDelegateWithIdentifier:@0 + webViewIdentifier:@1 + configurationIdentifier:configurationIdentifier + navigationAction:[OCMArg + isKindOfClass:[FWFWKNavigationActionData class]] + completion:OCMOCK_ANY]); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIViewHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIViewHostApiTests.m new file mode 100644 index 000000000000..65a24d97a39a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIViewHostApiTests.m @@ -0,0 +1,49 @@ +// 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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFUIViewHostApiTests : XCTestCase +@end + +@implementation FWFUIViewHostApiTests +- (void)testSetBackgroundColor { + UIView *mockUIView = OCMClassMock([UIView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockUIView withIdentifier:0]; + + FWFUIViewHostApiImpl *hostAPI = + [[FWFUIViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostAPI setBackgroundColorForViewWithIdentifier:@0 toValue:@123 error:&error]; + + OCMVerify([mockUIView setBackgroundColor:[UIColor colorWithRed:(123 >> 16 & 0xff) / 255.0 + green:(123 >> 8 & 0xff) / 255.0 + blue:(123 & 0xff) / 255.0 + alpha:(123 >> 24 & 0xff) / 255.0]]); + XCTAssertNil(error); +} + +- (void)testSetOpaque { + UIView *mockUIView = OCMClassMock([UIView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockUIView withIdentifier:0]; + + FWFUIViewHostApiImpl *hostAPI = + [[FWFUIViewHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostAPI setOpaqueForViewWithIdentifier:@0 isOpaque:@YES error:&error]; + OCMVerify([mockUIView setOpaque:YES]); + XCTAssertNil(error); +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUserContentControllerHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUserContentControllerHostApiTests.m new file mode 100644 index 000000000000..4f523e6da402 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUserContentControllerHostApiTests.m @@ -0,0 +1,127 @@ +// 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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFUserContentControllerHostApiTests : XCTestCase +@end + +@implementation FWFUserContentControllerHostApiTests +- (void)testCreateFromWebViewConfigurationWithIdentifier { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFUserContentControllerHostApiImpl *hostAPI = + [[FWFUserContentControllerHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + [instanceManager addDartCreatedInstance:[[WKWebViewConfiguration alloc] init] withIdentifier:0]; + + FlutterError *error; + [hostAPI createFromWebViewConfigurationWithIdentifier:@1 configurationIdentifier:@0 error:&error]; + WKUserContentController *userContentController = + (WKUserContentController *)[instanceManager instanceForIdentifier:1]; + XCTAssertTrue([userContentController isKindOfClass:[WKUserContentController class]]); + XCTAssertNil(error); +} + +- (void)testAddScriptMessageHandler { + WKUserContentController *mockUserContentController = + OCMClassMock([WKUserContentController class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockUserContentController withIdentifier:0]; + + FWFUserContentControllerHostApiImpl *hostAPI = + [[FWFUserContentControllerHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + id mockMessageHandler = + OCMProtocolMock(@protocol(WKScriptMessageHandler)); + [instanceManager addDartCreatedInstance:mockMessageHandler withIdentifier:1]; + + FlutterError *error; + [hostAPI addScriptMessageHandlerForControllerWithIdentifier:@0 + handlerIdentifier:@1 + ofName:@"apple" + error:&error]; + OCMVerify([mockUserContentController addScriptMessageHandler:mockMessageHandler name:@"apple"]); + XCTAssertNil(error); +} + +- (void)testRemoveScriptMessageHandler { + WKUserContentController *mockUserContentController = + OCMClassMock([WKUserContentController class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockUserContentController withIdentifier:0]; + + FWFUserContentControllerHostApiImpl *hostAPI = + [[FWFUserContentControllerHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostAPI removeScriptMessageHandlerForControllerWithIdentifier:@0 name:@"apple" error:&error]; + OCMVerify([mockUserContentController removeScriptMessageHandlerForName:@"apple"]); + XCTAssertNil(error); +} + +- (void)testRemoveAllScriptMessageHandlers API_AVAILABLE(ios(14.0)) { + WKUserContentController *mockUserContentController = + OCMClassMock([WKUserContentController class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockUserContentController withIdentifier:0]; + + FWFUserContentControllerHostApiImpl *hostAPI = + [[FWFUserContentControllerHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostAPI removeAllScriptMessageHandlersForControllerWithIdentifier:@0 error:&error]; + OCMVerify([mockUserContentController removeAllScriptMessageHandlers]); + XCTAssertNil(error); +} + +- (void)testAddUserScript { + WKUserContentController *mockUserContentController = + OCMClassMock([WKUserContentController class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockUserContentController withIdentifier:0]; + + FWFUserContentControllerHostApiImpl *hostAPI = + [[FWFUserContentControllerHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostAPI + addUserScriptForControllerWithIdentifier:@0 + userScript: + [FWFWKUserScriptData + makeWithSource:@"runAScript" + injectionTime: + [FWFWKUserScriptInjectionTimeEnumData + makeWithValue: + FWFWKUserScriptInjectionTimeEnumAtDocumentEnd] + isMainFrameOnly:@YES] + error:&error]; + + OCMVerify([mockUserContentController addUserScript:[OCMArg isKindOfClass:[WKUserScript class]]]); + XCTAssertNil(error); +} + +- (void)testRemoveAllUserScripts { + WKUserContentController *mockUserContentController = + OCMClassMock([WKUserContentController class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockUserContentController withIdentifier:0]; + + FWFUserContentControllerHostApiImpl *hostAPI = + [[FWFUserContentControllerHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostAPI removeAllUserScriptsForControllerWithIdentifier:@0 error:&error]; + OCMVerify([mockUserContentController removeAllUserScripts]); + XCTAssertNil(error); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewConfigurationHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewConfigurationHostApiTests.m new file mode 100644 index 000000000000..2ec74d0522dd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewConfigurationHostApiTests.m @@ -0,0 +1,92 @@ +// 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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFWebViewConfigurationHostApiTests : XCTestCase +@end + +@implementation FWFWebViewConfigurationHostApiTests +- (void)testCreateWithIdentifier { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFWebViewConfigurationHostApiImpl *hostAPI = [[FWFWebViewConfigurationHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI createWithIdentifier:@0 error:&error]; + WKWebViewConfiguration *configuration = + (WKWebViewConfiguration *)[instanceManager instanceForIdentifier:0]; + XCTAssertTrue([configuration isKindOfClass:[WKWebViewConfiguration class]]); + XCTAssertNil(error); +} + +- (void)testCreateFromWebViewWithIdentifier { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFWebViewConfigurationHostApiImpl *hostAPI = [[FWFWebViewConfigurationHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + WKWebView *mockWebView = OCMClassMock([WKWebView class]); + OCMStub([mockWebView configuration]).andReturn(OCMClassMock([WKWebViewConfiguration class])); + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FlutterError *error; + [hostAPI createFromWebViewWithIdentifier:@1 webViewIdentifier:@0 error:&error]; + WKWebViewConfiguration *configuration = + (WKWebViewConfiguration *)[instanceManager instanceForIdentifier:1]; + XCTAssertTrue([configuration isKindOfClass:[WKWebViewConfiguration class]]); + XCTAssertNil(error); +} + +- (void)testSetAllowsInlineMediaPlayback { + WKWebViewConfiguration *mockWebViewConfiguration = OCMClassMock([WKWebViewConfiguration class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebViewConfiguration withIdentifier:0]; + + FWFWebViewConfigurationHostApiImpl *hostAPI = [[FWFWebViewConfigurationHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:@0 + isAllowed:@NO + error:&error]; + OCMVerify([mockWebViewConfiguration setAllowsInlineMediaPlayback:NO]); + XCTAssertNil(error); +} + +- (void)testSetMediaTypesRequiringUserActionForPlayback { + WKWebViewConfiguration *mockWebViewConfiguration = OCMClassMock([WKWebViewConfiguration class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebViewConfiguration withIdentifier:0]; + + FWFWebViewConfigurationHostApiImpl *hostAPI = [[FWFWebViewConfigurationHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI + setMediaTypesRequiresUserActionForConfigurationWithIdentifier:@0 + forTypes:@[ + [FWFWKAudiovisualMediaTypeEnumData + makeWithValue: + FWFWKAudiovisualMediaTypeEnumAudio], + [FWFWKAudiovisualMediaTypeEnumData + makeWithValue: + FWFWKAudiovisualMediaTypeEnumVideo] + ] + error:&error]; + OCMVerify([mockWebViewConfiguration + setMediaTypesRequiringUserActionForPlayback:(WKAudiovisualMediaTypeAudio | + WKAudiovisualMediaTypeVideo)]); + XCTAssertNil(error); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewHostApiTests.m new file mode 100644 index 000000000000..a0026ca01f41 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewHostApiTests.m @@ -0,0 +1,469 @@ +// 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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +static bool feq(CGFloat a, CGFloat b) { return fabs(b - a) < FLT_EPSILON; } + +@interface FWFWebViewHostApiTests : XCTestCase +@end + +@implementation FWFWebViewHostApiTests +- (void)testCreateWithIdentifier { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + [instanceManager addDartCreatedInstance:[[WKWebViewConfiguration alloc] init] withIdentifier:0]; + + FlutterError *error; + [hostAPI createWithIdentifier:@1 configurationIdentifier:@0 error:&error]; + WKWebView *webView = (WKWebView *)[instanceManager instanceForIdentifier:1]; + XCTAssertTrue([webView isKindOfClass:[WKWebView class]]); + XCTAssertNil(error); +} + +- (void)testLoadRequest { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + FWFNSUrlRequestData *requestData = [FWFNSUrlRequestData makeWithUrl:@"https://www.flutter.dev" + httpMethod:@"get" + httpBody:nil + allHttpHeaderFields:@{@"a" : @"header"}]; + [hostAPI loadRequestForWebViewWithIdentifier:@0 request:requestData error:&error]; + + NSURL *url = [NSURL URLWithString:@"https://www.flutter.dev"]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + request.HTTPMethod = @"get"; + request.allHTTPHeaderFields = @{@"a" : @"header"}; + OCMVerify([mockWebView loadRequest:request]); + XCTAssertNil(error); +} + +- (void)testLoadRequestWithInvalidUrl { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + OCMReject([mockWebView loadRequest:OCMOCK_ANY]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + FWFNSUrlRequestData *requestData = [FWFNSUrlRequestData makeWithUrl:@"%invalidUrl%" + httpMethod:nil + httpBody:nil + allHttpHeaderFields:@{}]; + [hostAPI loadRequestForWebViewWithIdentifier:@0 request:requestData error:&error]; + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.code, @"FWFURLRequestParsingError"); + XCTAssertEqualObjects(error.message, @"Failed instantiating an NSURLRequest."); + XCTAssertEqualObjects(error.details, @"URL was: '%invalidUrl%'"); +} + +- (void)testSetCustomUserAgent { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI setUserAgentForWebViewWithIdentifier:@0 userAgent:@"userA" error:&error]; + OCMVerify([mockWebView setCustomUserAgent:@"userA"]); + XCTAssertNil(error); +} + +- (void)testURL { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + OCMStub([mockWebView URL]).andReturn([NSURL URLWithString:@"https://www.flutter.dev/"]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + XCTAssertEqualObjects([hostAPI URLForWebViewWithIdentifier:@0 error:&error], + @"https://www.flutter.dev/"); + XCTAssertNil(error); +} + +- (void)testCanGoBack { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + OCMStub([mockWebView canGoBack]).andReturn(YES); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + XCTAssertEqualObjects([hostAPI canGoBackForWebViewWithIdentifier:@0 error:&error], @YES); + XCTAssertNil(error); +} + +- (void)testSetUIDelegate { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + id mockDelegate = OCMProtocolMock(@protocol(WKUIDelegate)); + [instanceManager addDartCreatedInstance:mockDelegate withIdentifier:1]; + + FlutterError *error; + [hostAPI setUIDelegateForWebViewWithIdentifier:@0 delegateIdentifier:@1 error:&error]; + OCMVerify([mockWebView setUIDelegate:mockDelegate]); + XCTAssertNil(error); +} + +- (void)testSetNavigationDelegate { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + id mockDelegate = OCMProtocolMock(@protocol(WKNavigationDelegate)); + [instanceManager addDartCreatedInstance:mockDelegate withIdentifier:1]; + FlutterError *error; + + [hostAPI setNavigationDelegateForWebViewWithIdentifier:@0 delegateIdentifier:@1 error:&error]; + OCMVerify([mockWebView setNavigationDelegate:mockDelegate]); + XCTAssertNil(error); +} + +- (void)testEstimatedProgress { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + OCMStub([mockWebView estimatedProgress]).andReturn(34.0); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + XCTAssertEqualObjects([hostAPI estimatedProgressForWebViewWithIdentifier:@0 error:&error], @34.0); + XCTAssertNil(error); +} + +- (void)testloadHTMLString { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI loadHTMLForWebViewWithIdentifier:@0 + HTMLString:@"myString" + baseURL:@"myBaseUrl" + error:&error]; + OCMVerify([mockWebView loadHTMLString:@"myString" baseURL:[NSURL URLWithString:@"myBaseUrl"]]); + XCTAssertNil(error); +} + +- (void)testLoadFileURL { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI loadFileForWebViewWithIdentifier:@0 + fileURL:@"myFolder/apple.txt" + readAccessURL:@"myFolder" + error:&error]; + XCTAssertNil(error); + OCMVerify([mockWebView loadFileURL:[NSURL fileURLWithPath:@"myFolder/apple.txt" isDirectory:NO] + allowingReadAccessToURL:[NSURL fileURLWithPath:@"myFolder/" isDirectory:YES] + + ]); +} + +- (void)testLoadFlutterAsset { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFAssetManager *mockAssetManager = OCMClassMock([FWFAssetManager class]); + OCMStub([mockAssetManager lookupKeyForAsset:@"assets/index.html"]) + .andReturn(@"myFolder/assets/index.html"); + + NSBundle *mockBundle = OCMClassMock([NSBundle class]); + OCMStub([mockBundle URLForResource:@"myFolder/assets/index" withExtension:@"html"]) + .andReturn([NSURL URLWithString:@"webview_flutter/myFolder/assets/index.html"]); + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager + bundle:mockBundle + assetManager:mockAssetManager]; + + FlutterError *error; + [hostAPI loadAssetForWebViewWithIdentifier:@0 assetKey:@"assets/index.html" error:&error]; + + XCTAssertNil(error); + OCMVerify([mockWebView + loadFileURL:[NSURL URLWithString:@"webview_flutter/myFolder/assets/index.html"] + allowingReadAccessToURL:[NSURL URLWithString:@"webview_flutter/myFolder/assets/"]]); +} + +- (void)testCanGoForward { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + OCMStub([mockWebView canGoForward]).andReturn(NO); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + XCTAssertEqualObjects([hostAPI canGoForwardForWebViewWithIdentifier:@0 error:&error], @NO); + XCTAssertNil(error); +} + +- (void)testGoBack { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI goBackForWebViewWithIdentifier:@0 error:&error]; + OCMVerify([mockWebView goBack]); + XCTAssertNil(error); +} + +- (void)testGoForward { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI goForwardForWebViewWithIdentifier:@0 error:&error]; + OCMVerify([mockWebView goForward]); + XCTAssertNil(error); +} + +- (void)testReload { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI reloadWebViewWithIdentifier:@0 error:&error]; + OCMVerify([mockWebView reload]); + XCTAssertNil(error); +} + +- (void)testTitle { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + OCMStub([mockWebView title]).andReturn(@"myTitle"); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + XCTAssertEqualObjects([hostAPI titleForWebViewWithIdentifier:@0 error:&error], @"myTitle"); + XCTAssertNil(error); +} + +- (void)testSetAllowsBackForwardNavigationGestures { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI setAllowsBackForwardForWebViewWithIdentifier:@0 isAllowed:@YES error:&error]; + OCMVerify([mockWebView setAllowsBackForwardNavigationGestures:YES]); + XCTAssertNil(error); +} + +- (void)testEvaluateJavaScript { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + OCMStub([mockWebView + evaluateJavaScript:@"runJavaScript" + completionHandler:([OCMArg invokeBlockWithArgs:@"result", [NSNull null], nil])]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + NSString __block *returnValue; + FlutterError __block *returnError; + [hostAPI evaluateJavaScriptForWebViewWithIdentifier:@0 + javaScriptString:@"runJavaScript" + completion:^(id result, FlutterError *error) { + returnValue = result; + returnError = error; + }]; + + XCTAssertEqualObjects(returnValue, @"result"); + XCTAssertNil(returnError); +} + +- (void)testEvaluateJavaScriptReturnsNSErrorData { + FWFWebView *mockWebView = OCMClassMock([FWFWebView class]); + + OCMStub([mockWebView + evaluateJavaScript:@"runJavaScript" + completionHandler:([OCMArg invokeBlockWithArgs:[NSNull null], + [NSError errorWithDomain:@"errorDomain" + code:0 + userInfo:@{ + NSLocalizedDescriptionKey : + @"description" + }], + nil])]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:0]; + + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + NSString __block *returnValue; + FlutterError __block *returnError; + [hostAPI evaluateJavaScriptForWebViewWithIdentifier:@0 + javaScriptString:@"runJavaScript" + completion:^(id result, FlutterError *error) { + returnValue = result; + returnError = error; + }]; + + XCTAssertNil(returnValue); + FWFNSErrorData *errorData = returnError.details; + XCTAssertTrue([errorData isKindOfClass:[FWFNSErrorData class]]); + XCTAssertEqualObjects(errorData.code, @0); + XCTAssertEqualObjects(errorData.domain, @"errorDomain"); + XCTAssertEqualObjects(errorData.localizedDescription, @"description"); +} + +- (void)testWebViewContentInsetBehaviorShouldBeNeverOnIOS11 API_AVAILABLE(ios(11.0)) { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + [instanceManager addDartCreatedInstance:[[WKWebViewConfiguration alloc] init] withIdentifier:0]; + + FlutterError *error; + [hostAPI createWithIdentifier:@1 configurationIdentifier:@0 error:&error]; + FWFWebView *webView = (FWFWebView *)[instanceManager instanceForIdentifier:1]; + + XCTAssertEqual(webView.scrollView.contentInsetAdjustmentBehavior, + UIScrollViewContentInsetAdjustmentNever); +} + +- (void)testScrollViewsAutomaticallyAdjustsScrollIndicatorInsetsShouldbeNoOnIOS13 API_AVAILABLE( + ios(13.0)) { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + [instanceManager addDartCreatedInstance:[[WKWebViewConfiguration alloc] init] withIdentifier:0]; + + FlutterError *error; + [hostAPI createWithIdentifier:@1 configurationIdentifier:@0 error:&error]; + FWFWebView *webView = (FWFWebView *)[instanceManager instanceForIdentifier:1]; + + XCTAssertFalse(webView.scrollView.automaticallyAdjustsScrollIndicatorInsets); +} + +- (void)testContentInsetsSumAlwaysZeroAfterSetFrame { + FWFWebView *webView = [[FWFWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + configuration:[[WKWebViewConfiguration alloc] init]]; + + webView.scrollView.contentInset = UIEdgeInsetsMake(0, 0, 300, 0); + XCTAssertFalse(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + + webView.frame = CGRectMake(0, 0, 300, 200); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 200))); + + if (@available(iOS 11, *)) { + // After iOS 11, we need to make sure the contentInset compensates the adjustedContentInset. + UIScrollView *partialMockScrollView = OCMPartialMock(webView.scrollView); + UIEdgeInsets insetToAdjust = UIEdgeInsetsMake(0, 0, 300, 0); + OCMStub(partialMockScrollView.adjustedContentInset).andReturn(insetToAdjust); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + + webView.frame = CGRectMake(0, 0, 300, 100); + XCTAssertTrue(feq(webView.scrollView.contentInset.bottom, -insetToAdjust.bottom)); + XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 100))); + } +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebsiteDataStoreHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebsiteDataStoreHostApiTests.m new file mode 100644 index 000000000000..c518f55194c4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebsiteDataStoreHostApiTests.m @@ -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 Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +#import + +@interface FWFWebsiteDataStoreHostApiTests : XCTestCase +@end + +@implementation FWFWebsiteDataStoreHostApiTests +- (void)testCreateFromWebViewConfigurationWithIdentifier { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFWebsiteDataStoreHostApiImpl *hostAPI = + [[FWFWebsiteDataStoreHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + [instanceManager addDartCreatedInstance:[[WKWebViewConfiguration alloc] init] withIdentifier:0]; + + FlutterError *error; + [hostAPI createFromWebViewConfigurationWithIdentifier:@1 configurationIdentifier:@0 error:&error]; + WKWebsiteDataStore *dataStore = (WKWebsiteDataStore *)[instanceManager instanceForIdentifier:1]; + XCTAssertTrue([dataStore isKindOfClass:[WKWebsiteDataStore class]]); + XCTAssertNil(error); +} + +- (void)testCreateDefaultDataStoreWithIdentifier { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + FWFWebsiteDataStoreHostApiImpl *hostAPI = + [[FWFWebsiteDataStoreHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + FlutterError *error; + [hostAPI createDefaultDataStoreWithIdentifier:@0 error:&error]; + WKWebsiteDataStore *dataStore = (WKWebsiteDataStore *)[instanceManager instanceForIdentifier:0]; + XCTAssertEqualObjects(dataStore, [WKWebsiteDataStore defaultDataStore]); + XCTAssertNil(error); +} + +- (void)testRemoveDataOfTypes { + WKWebsiteDataStore *mockWebsiteDataStore = OCMClassMock([WKWebsiteDataStore class]); + + WKWebsiteDataRecord *mockDataRecord = OCMClassMock([WKWebsiteDataRecord class]); + OCMStub([mockWebsiteDataStore + fetchDataRecordsOfTypes:[NSSet setWithObject:WKWebsiteDataTypeLocalStorage] + completionHandler:([OCMArg invokeBlockWithArgs:@[ mockDataRecord ], nil])]); + + OCMStub([mockWebsiteDataStore + removeDataOfTypes:[NSSet setWithObject:WKWebsiteDataTypeLocalStorage] + modifiedSince:[NSDate dateWithTimeIntervalSince1970:45.0] + completionHandler:([OCMArg invokeBlock])]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebsiteDataStore withIdentifier:0]; + + FWFWebsiteDataStoreHostApiImpl *hostAPI = + [[FWFWebsiteDataStoreHostApiImpl alloc] initWithInstanceManager:instanceManager]; + + NSNumber __block *returnValue; + FlutterError *__block blockError; + [hostAPI removeDataFromDataStoreWithIdentifier:@0 + ofTypes:@[ + [FWFWKWebsiteDataTypeEnumData + makeWithValue:FWFWKWebsiteDataTypeEnumLocalStorage] + ] + modifiedSince:@45.0 + completion:^(NSNumber *result, FlutterError *error) { + returnValue = result; + blockError = error; + }]; + XCTAssertEqualObjects(returnValue, @YES); + // Asserts whether the NSNumber will be deserialized by the standard codec as a boolean. + XCTAssertEqual(CFGetTypeID((__bridge CFTypeRef)(returnValue)), CFBooleanGetTypeID()); + XCTAssertNil(blockError); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h deleted file mode 100644 index b18e58b57cb3..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h +++ /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 -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FLTCookieManager : NSObject - -+ (FLTCookieManager *)instance; - -- (void)setCookiesForData:(NSArray *)cookies; - -- (void)setCookieForData:(NSDictionary *)cookie; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h deleted file mode 100644 index 6531931c4cf4..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h +++ /dev/null @@ -1,26 +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 -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FLTWKNavigationDelegate : NSObject - -- (instancetype)initWithChannel:(FlutterMethodChannel *)channel; - -/** - * Whether to delegate navigation decisions over the method channel. - */ -@property(nonatomic, assign) BOOL hasDartNavigationDelegate; - -/** - * Whether to allow zoom functionality on the WebView. - */ -@property(nonatomic, assign) BOOL shouldEnableZoom; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m deleted file mode 100644 index 125d3cabdcf1..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m +++ /dev/null @@ -1,127 +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 "FLTWKNavigationDelegate.h" - -@implementation FLTWKNavigationDelegate { - FlutterMethodChannel *_methodChannel; -} - -- (instancetype)initWithChannel:(FlutterMethodChannel *)channel { - self = [super init]; - if (self) { - _methodChannel = channel; - } - return self; -} - -#pragma mark - WKNavigationDelegate conformance - -- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { - [_methodChannel invokeMethod:@"onPageStarted" arguments:@{@"url" : webView.URL.absoluteString}]; -} - -- (void)webView:(WKWebView *)webView - decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction - decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { - if (!self.hasDartNavigationDelegate) { - decisionHandler(WKNavigationActionPolicyAllow); - return; - } - NSDictionary *arguments = @{ - @"url" : navigationAction.request.URL.absoluteString, - @"isForMainFrame" : @(navigationAction.targetFrame.isMainFrame) - }; - [_methodChannel invokeMethod:@"navigationRequest" - arguments:arguments - result:^(id _Nullable result) { - if ([result isKindOfClass:[FlutterError class]]) { - NSLog(@"navigationRequest has unexpectedly completed with an error, " - @"allowing navigation."); - decisionHandler(WKNavigationActionPolicyAllow); - return; - } - if (result == FlutterMethodNotImplemented) { - NSLog(@"navigationRequest was unexepectedly not implemented: %@, " - @"allowing navigation.", - result); - decisionHandler(WKNavigationActionPolicyAllow); - return; - } - if (![result isKindOfClass:[NSNumber class]]) { - NSLog(@"navigationRequest unexpectedly returned a non boolean value: " - @"%@, allowing navigation.", - result); - decisionHandler(WKNavigationActionPolicyAllow); - return; - } - NSNumber *typedResult = result; - decisionHandler([typedResult boolValue] ? WKNavigationActionPolicyAllow - : WKNavigationActionPolicyCancel); - }]; -} - -- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { - if (!self.shouldEnableZoom) { - NSString *source = - @"var meta = document.createElement('meta');" - @"meta.name = 'viewport';" - @"meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0," - @"user-scalable=no';" - @"var head = document.getElementsByTagName('head')[0];head.appendChild(meta);"; - - [webView evaluateJavaScript:source completionHandler:nil]; - } - - [_methodChannel invokeMethod:@"onPageFinished" arguments:@{@"url" : webView.URL.absoluteString}]; -} - -+ (id)errorCodeToString:(NSUInteger)code { - switch (code) { - case WKErrorUnknown: - return @"unknown"; - case WKErrorWebContentProcessTerminated: - return @"webContentProcessTerminated"; - case WKErrorWebViewInvalidated: - return @"webViewInvalidated"; - case WKErrorJavaScriptExceptionOccurred: - return @"javaScriptExceptionOccurred"; - case WKErrorJavaScriptResultTypeIsUnsupported: - return @"javaScriptResultTypeIsUnsupported"; - } - - return [NSNull null]; -} - -- (void)onWebResourceError:(NSError *)error { - [_methodChannel invokeMethod:@"onWebResourceError" - arguments:@{ - @"errorCode" : @(error.code), - @"domain" : error.domain, - @"description" : error.description, - @"errorType" : [FLTWKNavigationDelegate errorCodeToString:error.code], - }]; -} - -- (void)webView:(WKWebView *)webView - didFailNavigation:(WKNavigation *)navigation - withError:(NSError *)error { - [self onWebResourceError:error]; -} - -- (void)webView:(WKWebView *)webView - didFailProvisionalNavigation:(WKNavigation *)navigation - withError:(NSError *)error { - [self onWebResourceError:error]; -} - -- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { - NSError *contentProcessTerminatedError = - [[NSError alloc] initWithDomain:WKErrorDomain - code:WKErrorWebContentProcessTerminated - userInfo:nil]; - [self onWebResourceError:contentProcessTerminatedError]; -} - -@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h deleted file mode 100644 index 96af4ef6c578..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h +++ /dev/null @@ -1,19 +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 -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FLTWKProgressionDelegate : NSObject - -- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel; - -- (void)stopObservingProgress:(WKWebView *)webView; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m deleted file mode 100644 index 8e7af4649aa0..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m +++ /dev/null @@ -1,41 +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 "FLTWKProgressionDelegate.h" - -NSString *const FLTWKEstimatedProgressKeyPath = @"estimatedProgress"; - -@implementation FLTWKProgressionDelegate { - FlutterMethodChannel *_methodChannel; -} - -- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel { - self = [super init]; - if (self) { - _methodChannel = channel; - [webView addObserver:self - forKeyPath:FLTWKEstimatedProgressKeyPath - options:NSKeyValueObservingOptionNew - context:nil]; - } - return self; -} - -- (void)stopObservingProgress:(WKWebView *)webView { - [webView removeObserver:self forKeyPath:FLTWKEstimatedProgressKeyPath]; -} - -- (void)observeValueForKeyPath:(NSString *)keyPath - ofObject:(id)object - change:(NSDictionary *)change - context:(void *)context { - if ([keyPath isEqualToString:FLTWKEstimatedProgressKeyPath]) { - NSNumber *newValue = - change[NSKeyValueChangeNewKey] ?: 0; // newValue is anywhere between 0.0 and 1.0 - int newValueAsInt = [newValue floatValue] * 100; // Anywhere between 0 and 100 - [_methodChannel invokeMethod:@"onProgress" arguments:@{@"progress" : @(newValueAsInt)}]; - } -} - -@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m index a4e87ba5177a..5795018b2043 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m @@ -3,17 +3,112 @@ // found in the LICENSE file. #import "FLTWebViewFlutterPlugin.h" -#import "FLTCookieManager.h" -#import "FlutterWebView.h" +#import "FWFGeneratedWebKitApis.h" +#import "FWFHTTPCookieStoreHostApi.h" +#import "FWFInstanceManager.h" +#import "FWFNavigationDelegateHostApi.h" +#import "FWFObjectHostApi.h" +#import "FWFPreferencesHostApi.h" +#import "FWFScriptMessageHandlerHostApi.h" +#import "FWFScrollViewHostApi.h" +#import "FWFUIDelegateHostApi.h" +#import "FWFUIViewHostApi.h" +#import "FWFUserContentControllerHostApi.h" +#import "FWFWebViewConfigurationHostApi.h" +#import "FWFWebViewHostApi.h" +#import "FWFWebsiteDataStoreHostApi.h" + +@interface FWFWebViewFactory : NSObject +@property(nonatomic, weak) FWFInstanceManager *instanceManager; + +- (instancetype)initWithManager:(FWFInstanceManager *)manager; +@end + +@implementation FWFWebViewFactory +- (instancetype)initWithManager:(FWFInstanceManager *)manager { + self = [self init]; + if (self) { + _instanceManager = manager; + } + return self; +} + +- (NSObject *)createArgsCodec { + return [FlutterStandardMessageCodec sharedInstance]; +} + +- (NSObject *)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + NSNumber *identifier = (NSNumber *)args; + FWFWebView *webView = + (FWFWebView *)[self.instanceManager instanceForIdentifier:identifier.longValue]; + webView.frame = frame; + return webView; +} + +@end @implementation FLTWebViewFlutterPlugin + (void)registerWithRegistrar:(NSObject *)registrar { - [FLTCookieManager registerWithRegistrar:registrar]; - FLTWebViewFactory *webviewFactory = - [[FLTWebViewFactory alloc] initWithMessenger:registrar.messenger - cookieManager:[FLTCookieManager instance]]; + FWFInstanceManager *instanceManager = + [[FWFInstanceManager alloc] initWithDeallocCallback:^(long identifier) { + FWFObjectFlutterApiImpl *objectApi = [[FWFObjectFlutterApiImpl alloc] + initWithBinaryMessenger:registrar.messenger + instanceManager:[[FWFInstanceManager alloc] init]]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [objectApi disposeObjectWithIdentifier:@(identifier) + completion:^(NSError *error) { + NSAssert(!error, @"%@", error); + }]; + }); + }]; + FWFWKHttpCookieStoreHostApiSetup( + registrar.messenger, + [[FWFHTTPCookieStoreHostApiImpl alloc] initWithInstanceManager:instanceManager]); + FWFWKNavigationDelegateHostApiSetup( + registrar.messenger, + [[FWFNavigationDelegateHostApiImpl alloc] initWithBinaryMessenger:registrar.messenger + instanceManager:instanceManager]); + FWFNSObjectHostApiSetup(registrar.messenger, + [[FWFObjectHostApiImpl alloc] initWithInstanceManager:instanceManager]); + FWFWKPreferencesHostApiSetup(registrar.messenger, [[FWFPreferencesHostApiImpl alloc] + initWithInstanceManager:instanceManager]); + FWFWKScriptMessageHandlerHostApiSetup( + registrar.messenger, + [[FWFScriptMessageHandlerHostApiImpl alloc] initWithBinaryMessenger:registrar.messenger + instanceManager:instanceManager]); + FWFUIScrollViewHostApiSetup(registrar.messenger, [[FWFScrollViewHostApiImpl alloc] + initWithInstanceManager:instanceManager]); + FWFWKUIDelegateHostApiSetup(registrar.messenger, [[FWFUIDelegateHostApiImpl alloc] + initWithBinaryMessenger:registrar.messenger + instanceManager:instanceManager]); + FWFUIViewHostApiSetup(registrar.messenger, + [[FWFUIViewHostApiImpl alloc] initWithInstanceManager:instanceManager]); + FWFWKUserContentControllerHostApiSetup( + registrar.messenger, + [[FWFUserContentControllerHostApiImpl alloc] initWithInstanceManager:instanceManager]); + FWFWKWebsiteDataStoreHostApiSetup( + registrar.messenger, + [[FWFWebsiteDataStoreHostApiImpl alloc] initWithInstanceManager:instanceManager]); + FWFWKWebViewConfigurationHostApiSetup( + registrar.messenger, + [[FWFWebViewConfigurationHostApiImpl alloc] initWithBinaryMessenger:registrar.messenger + instanceManager:instanceManager]); + FWFWKWebViewHostApiSetup(registrar.messenger, [[FWFWebViewHostApiImpl alloc] + initWithBinaryMessenger:registrar.messenger + instanceManager:instanceManager]); + + FWFWebViewFactory *webviewFactory = [[FWFWebViewFactory alloc] initWithManager:instanceManager]; [registrar registerViewFactory:webviewFactory withId:@"plugins.flutter.io/webview"]; + + // InstanceManager is published so that a strong reference is maintained. + [registrar publish:instanceManager]; } +- (void)detachFromEngineForRegistrar:(NSObject *)registrar { + [registrar publish:[NSNull null]]; +} @end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h new file mode 100644 index 000000000000..2863048726a9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h @@ -0,0 +1,154 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FWFGeneratedWebKitApis.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Converts an FWFNSUrlRequestData to an NSURLRequest. + * + * @param data The data object containing information to create an NSURLRequest. + * + * @return An NSURLRequest or nil if data could not be converted. + */ +extern NSURLRequest *_Nullable FWFNSURLRequestFromRequestData(FWFNSUrlRequestData *data); + +/** + * Converts an FWFNSHttpCookieData to an NSHTTPCookie. + * + * @param data The data object containing information to create an NSHTTPCookie. + * + * @return An NSHTTPCookie or nil if data could not be converted. + */ +extern NSHTTPCookie *_Nullable FWFNSHTTPCookieFromCookieData(FWFNSHttpCookieData *data); + +/** + * Converts an FWFNSKeyValueObservingOptionsEnumData to an NSKeyValueObservingOptions. + * + * @param data The data object containing information to create an NSKeyValueObservingOptions. + * + * @return An NSKeyValueObservingOptions or -1 if data could not be converted. + */ +extern NSKeyValueObservingOptions FWFNSKeyValueObservingOptionsFromEnumData( + FWFNSKeyValueObservingOptionsEnumData *data); + +/** + * Converts an FWFNSHTTPCookiePropertyKeyEnumData to an NSHTTPCookiePropertyKey. + * + * @param data The data object containing information to create an NSHTTPCookiePropertyKey. + * + * @return An NSHttpCookiePropertyKey or nil if data could not be converted. + */ +extern NSHTTPCookiePropertyKey _Nullable FWFNSHTTPCookiePropertyKeyFromEnumData( + FWFNSHttpCookiePropertyKeyEnumData *data); + +/** + * Converts a WKUserScriptData to a WKUserScript. + * + * @param data The data object containing information to create a WKUserScript. + * + * @return A WKUserScript or nil if data could not be converted. + */ +extern WKUserScript *FWFWKUserScriptFromScriptData(FWFWKUserScriptData *data); + +/** + * Converts an FWFWKUserScriptInjectionTimeEnumData to a WKUserScriptInjectionTime. + * + * @param data The data object containing information to create a WKUserScriptInjectionTime. + * + * @return A WKUserScriptInjectionTime or -1 if data could not be converted. + */ +extern WKUserScriptInjectionTime FWFWKUserScriptInjectionTimeFromEnumData( + FWFWKUserScriptInjectionTimeEnumData *data); + +/** + * Converts an FWFWKAudiovisualMediaTypeEnumData to a WKAudiovisualMediaTypes. + * + * @param data The data object containing information to create a WKAudiovisualMediaTypes. + * + * @return A WKAudiovisualMediaType or -1 if data could not be converted. + */ +API_AVAILABLE(ios(10.0)) +extern WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( + FWFWKAudiovisualMediaTypeEnumData *data); + +/** + * Converts an FWFWKWebsiteDataTypeEnumData to a WKWebsiteDataType. + * + * @param data The data object containing information to create a WKWebsiteDataType. + * + * @return A WKWebsiteDataType or nil if data could not be converted. + */ +extern NSString *_Nullable FWFWKWebsiteDataTypeFromEnumData(FWFWKWebsiteDataTypeEnumData *data); + +/** + * Converts a WKNavigationAction to an FWFWKNavigationActionData. + * + * @param action The object containing information to create a WKNavigationActionData. + * + * @return A FWFWKNavigationActionData. + */ +extern FWFWKNavigationActionData *FWFWKNavigationActionDataFromNavigationAction( + WKNavigationAction *action); + +/** + * Converts a NSURLRequest to an FWFNSUrlRequestData. + * + * @param request The object containing information to create a WKNavigationActionData. + * + * @return A FWFNSUrlRequestData. + */ +extern FWFNSUrlRequestData *FWFNSUrlRequestDataFromNSURLRequest(NSURLRequest *request); + +/** + * Converts a WKFrameInfo to an FWFWKFrameInfoData. + * + * @param info The object containing information to create a FWFWKFrameInfoData. + * + * @return A FWFWKFrameInfoData. + */ +extern FWFWKFrameInfoData *FWFWKFrameInfoDataFromWKFrameInfo(WKFrameInfo *info); + +/** + * Converts an FWFWKNavigationActionPolicyEnumData to a WKNavigationActionPolicy. + * + * @param data The data object containing information to create a WKNavigationActionPolicy. + * + * @return A WKNavigationActionPolicy or -1 if data could not be converted. + */ +extern WKNavigationActionPolicy FWFWKNavigationActionPolicyFromEnumData( + FWFWKNavigationActionPolicyEnumData *data); + +/** + * Converts a NSError to an FWFNSErrorData. + * + * @param error The object containing information to create a FWFNSErrorData. + * + * @return A FWFNSErrorData. + */ +extern FWFNSErrorData *FWFNSErrorDataFromNSError(NSError *error); + +/** + * Converts an NSKeyValueChangeKey to a FWFNSKeyValueChangeKeyEnumData. + * + * @param key The data object containing information to create a FWFNSKeyValueChangeKeyEnumData. + * + * @return A FWFNSKeyValueChangeKeyEnumData or nil if data could not be converted. + */ +extern FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNSKeyValueChangeKey( + NSKeyValueChangeKey key); + +/** + * Converts a WKScriptMessage to an FWFWKScriptMessageData. + * + * @param message The object containing information to create a FWFWKScriptMessageData. + * + * @return A FWFWKScriptMessageData. + */ +extern FWFWKScriptMessageData *FWFWKScriptMessageDataFromWKScriptMessage(WKScriptMessage *message); + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m new file mode 100644 index 000000000000..8ecc9d303000 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m @@ -0,0 +1,220 @@ +// 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 "FWFDataConverters.h" + +#import + +NSURLRequest *_Nullable FWFNSURLRequestFromRequestData(FWFNSUrlRequestData *data) { + NSURL *url = [NSURL URLWithString:data.url]; + if (!url) { + return nil; + } + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + if (!request) { + return nil; + } + + if (data.httpMethod) { + [request setHTTPMethod:data.httpMethod]; + } + if (data.httpBody) { + [request setHTTPBody:data.httpBody.data]; + } + [request setAllHTTPHeaderFields:data.allHttpHeaderFields]; + + return request; +} + +extern NSHTTPCookie *_Nullable FWFNSHTTPCookieFromCookieData(FWFNSHttpCookieData *data) { + NSMutableDictionary *properties = [NSMutableDictionary dictionary]; + for (int i = 0; i < data.propertyKeys.count; i++) { + NSHTTPCookiePropertyKey cookieKey = + FWFNSHTTPCookiePropertyKeyFromEnumData(data.propertyKeys[i]); + if (!cookieKey) { + // Some keys aren't supported on all versions, so this ignores keys + // that require a higher version or are unsupported. + continue; + } + [properties setObject:data.propertyValues[i] forKey:cookieKey]; + } + return [NSHTTPCookie cookieWithProperties:properties]; +} + +NSKeyValueObservingOptions FWFNSKeyValueObservingOptionsFromEnumData( + FWFNSKeyValueObservingOptionsEnumData *data) { + switch (data.value) { + case FWFNSKeyValueObservingOptionsEnumNewValue: + return NSKeyValueObservingOptionNew; + case FWFNSKeyValueObservingOptionsEnumOldValue: + return NSKeyValueObservingOptionOld; + case FWFNSKeyValueObservingOptionsEnumInitialValue: + return NSKeyValueObservingOptionInitial; + case FWFNSKeyValueObservingOptionsEnumPriorNotification: + return NSKeyValueObservingOptionPrior; + } + + return -1; +} + +NSHTTPCookiePropertyKey _Nullable FWFNSHTTPCookiePropertyKeyFromEnumData( + FWFNSHttpCookiePropertyKeyEnumData *data) { + switch (data.value) { + case FWFNSHttpCookiePropertyKeyEnumComment: + return NSHTTPCookieComment; + case FWFNSHttpCookiePropertyKeyEnumCommentUrl: + return NSHTTPCookieCommentURL; + case FWFNSHttpCookiePropertyKeyEnumDiscard: + return NSHTTPCookieDiscard; + case FWFNSHttpCookiePropertyKeyEnumDomain: + return NSHTTPCookieDomain; + case FWFNSHttpCookiePropertyKeyEnumExpires: + return NSHTTPCookieExpires; + case FWFNSHttpCookiePropertyKeyEnumMaximumAge: + return NSHTTPCookieMaximumAge; + case FWFNSHttpCookiePropertyKeyEnumName: + return NSHTTPCookieName; + case FWFNSHttpCookiePropertyKeyEnumOriginUrl: + return NSHTTPCookieOriginURL; + case FWFNSHttpCookiePropertyKeyEnumPath: + return NSHTTPCookiePath; + case FWFNSHttpCookiePropertyKeyEnumPort: + return NSHTTPCookiePort; + case FWFNSHttpCookiePropertyKeyEnumSameSitePolicy: + if (@available(iOS 13.0, *)) { + return NSHTTPCookieSameSitePolicy; + } else { + return nil; + } + case FWFNSHttpCookiePropertyKeyEnumSecure: + return NSHTTPCookieSecure; + case FWFNSHttpCookiePropertyKeyEnumValue: + return NSHTTPCookieValue; + case FWFNSHttpCookiePropertyKeyEnumVersion: + return NSHTTPCookieVersion; + } + + return nil; +} + +extern WKUserScript *FWFWKUserScriptFromScriptData(FWFWKUserScriptData *data) { + return [[WKUserScript alloc] + initWithSource:data.source + injectionTime:FWFWKUserScriptInjectionTimeFromEnumData(data.injectionTime) + forMainFrameOnly:data.isMainFrameOnly.boolValue]; +} + +WKUserScriptInjectionTime FWFWKUserScriptInjectionTimeFromEnumData( + FWFWKUserScriptInjectionTimeEnumData *data) { + switch (data.value) { + case FWFWKUserScriptInjectionTimeEnumAtDocumentStart: + return WKUserScriptInjectionTimeAtDocumentStart; + case FWFWKUserScriptInjectionTimeEnumAtDocumentEnd: + return WKUserScriptInjectionTimeAtDocumentEnd; + } + + return -1; +} + +API_AVAILABLE(ios(10.0)) +WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( + FWFWKAudiovisualMediaTypeEnumData *data) { + switch (data.value) { + case FWFWKAudiovisualMediaTypeEnumNone: + return WKAudiovisualMediaTypeNone; + case FWFWKAudiovisualMediaTypeEnumAudio: + return WKAudiovisualMediaTypeAudio; + case FWFWKAudiovisualMediaTypeEnumVideo: + return WKAudiovisualMediaTypeVideo; + case FWFWKAudiovisualMediaTypeEnumAll: + return WKAudiovisualMediaTypeAll; + } + + return -1; +} + +NSString *_Nullable FWFWKWebsiteDataTypeFromEnumData(FWFWKWebsiteDataTypeEnumData *data) { + switch (data.value) { + case FWFWKWebsiteDataTypeEnumCookies: + return WKWebsiteDataTypeCookies; + case FWFWKWebsiteDataTypeEnumMemoryCache: + return WKWebsiteDataTypeMemoryCache; + case FWFWKWebsiteDataTypeEnumDiskCache: + return WKWebsiteDataTypeDiskCache; + case FWFWKWebsiteDataTypeEnumOfflineWebApplicationCache: + return WKWebsiteDataTypeOfflineWebApplicationCache; + case FWFWKWebsiteDataTypeEnumLocalStorage: + return WKWebsiteDataTypeLocalStorage; + case FWFWKWebsiteDataTypeEnumSessionStorage: + return WKWebsiteDataTypeSessionStorage; + case FWFWKWebsiteDataTypeEnumWebSQLDatabases: + return WKWebsiteDataTypeWebSQLDatabases; + case FWFWKWebsiteDataTypeEnumIndexedDBDatabases: + return WKWebsiteDataTypeIndexedDBDatabases; + } + + return nil; +} + +FWFWKNavigationActionData *FWFWKNavigationActionDataFromNavigationAction( + WKNavigationAction *action) { + return [FWFWKNavigationActionData + makeWithRequest:FWFNSUrlRequestDataFromNSURLRequest(action.request) + targetFrame:FWFWKFrameInfoDataFromWKFrameInfo(action.targetFrame)]; +} + +FWFNSUrlRequestData *FWFNSUrlRequestDataFromNSURLRequest(NSURLRequest *request) { + return [FWFNSUrlRequestData + makeWithUrl:request.URL.absoluteString + httpMethod:request.HTTPMethod + httpBody:request.HTTPBody + ? [FlutterStandardTypedData typedDataWithBytes:request.HTTPBody] + : nil + allHttpHeaderFields:request.allHTTPHeaderFields ? request.allHTTPHeaderFields : @{}]; +} + +FWFWKFrameInfoData *FWFWKFrameInfoDataFromWKFrameInfo(WKFrameInfo *info) { + return [FWFWKFrameInfoData makeWithIsMainFrame:@(info.isMainFrame)]; +} + +WKNavigationActionPolicy FWFWKNavigationActionPolicyFromEnumData( + FWFWKNavigationActionPolicyEnumData *data) { + switch (data.value) { + case FWFWKNavigationActionPolicyEnumAllow: + return WKNavigationActionPolicyAllow; + case FWFWKNavigationActionPolicyEnumCancel: + return WKNavigationActionPolicyCancel; + } + + return -1; +} + +FWFNSErrorData *FWFNSErrorDataFromNSError(NSError *error) { + return [FWFNSErrorData makeWithCode:@(error.code) + domain:error.domain + localizedDescription:error.localizedDescription]; +} + +FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNSKeyValueChangeKey( + NSKeyValueChangeKey key) { + if ([key isEqualToString:NSKeyValueChangeIndexesKey]) { + return [FWFNSKeyValueChangeKeyEnumData makeWithValue:FWFNSKeyValueChangeKeyEnumIndexes]; + } else if ([key isEqualToString:NSKeyValueChangeKindKey]) { + return [FWFNSKeyValueChangeKeyEnumData makeWithValue:FWFNSKeyValueChangeKeyEnumKind]; + } else if ([key isEqualToString:NSKeyValueChangeNewKey]) { + return [FWFNSKeyValueChangeKeyEnumData makeWithValue:FWFNSKeyValueChangeKeyEnumNewValue]; + } else if ([key isEqualToString:NSKeyValueChangeNotificationIsPriorKey]) { + return [FWFNSKeyValueChangeKeyEnumData + makeWithValue:FWFNSKeyValueChangeKeyEnumNotificationIsPrior]; + } else if ([key isEqualToString:NSKeyValueChangeOldKey]) { + return [FWFNSKeyValueChangeKeyEnumData makeWithValue:FWFNSKeyValueChangeKeyEnumOldValue]; + } + + return nil; +} + +FWFWKScriptMessageData *FWFWKScriptMessageDataFromWKScriptMessage(WKScriptMessage *message) { + return [FWFWKScriptMessageData makeWithName:message.name body:message.body]; +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h new file mode 100644 index 000000000000..8cbd2c7c194c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h @@ -0,0 +1,557 @@ +// 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.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, FWFNSKeyValueObservingOptionsEnum) { + FWFNSKeyValueObservingOptionsEnumNewValue = 0, + FWFNSKeyValueObservingOptionsEnumOldValue = 1, + FWFNSKeyValueObservingOptionsEnumInitialValue = 2, + FWFNSKeyValueObservingOptionsEnumPriorNotification = 3, +}; + +typedef NS_ENUM(NSUInteger, FWFNSKeyValueChangeEnum) { + FWFNSKeyValueChangeEnumSetting = 0, + FWFNSKeyValueChangeEnumInsertion = 1, + FWFNSKeyValueChangeEnumRemoval = 2, + FWFNSKeyValueChangeEnumReplacement = 3, +}; + +typedef NS_ENUM(NSUInteger, FWFNSKeyValueChangeKeyEnum) { + FWFNSKeyValueChangeKeyEnumIndexes = 0, + FWFNSKeyValueChangeKeyEnumKind = 1, + FWFNSKeyValueChangeKeyEnumNewValue = 2, + FWFNSKeyValueChangeKeyEnumNotificationIsPrior = 3, + FWFNSKeyValueChangeKeyEnumOldValue = 4, +}; + +typedef NS_ENUM(NSUInteger, FWFWKUserScriptInjectionTimeEnum) { + FWFWKUserScriptInjectionTimeEnumAtDocumentStart = 0, + FWFWKUserScriptInjectionTimeEnumAtDocumentEnd = 1, +}; + +typedef NS_ENUM(NSUInteger, FWFWKAudiovisualMediaTypeEnum) { + FWFWKAudiovisualMediaTypeEnumNone = 0, + FWFWKAudiovisualMediaTypeEnumAudio = 1, + FWFWKAudiovisualMediaTypeEnumVideo = 2, + FWFWKAudiovisualMediaTypeEnumAll = 3, +}; + +typedef NS_ENUM(NSUInteger, FWFWKWebsiteDataTypeEnum) { + FWFWKWebsiteDataTypeEnumCookies = 0, + FWFWKWebsiteDataTypeEnumMemoryCache = 1, + FWFWKWebsiteDataTypeEnumDiskCache = 2, + FWFWKWebsiteDataTypeEnumOfflineWebApplicationCache = 3, + FWFWKWebsiteDataTypeEnumLocalStorage = 4, + FWFWKWebsiteDataTypeEnumSessionStorage = 5, + FWFWKWebsiteDataTypeEnumWebSQLDatabases = 6, + FWFWKWebsiteDataTypeEnumIndexedDBDatabases = 7, +}; + +typedef NS_ENUM(NSUInteger, FWFWKNavigationActionPolicyEnum) { + FWFWKNavigationActionPolicyEnumAllow = 0, + FWFWKNavigationActionPolicyEnumCancel = 1, +}; + +typedef NS_ENUM(NSUInteger, FWFNSHttpCookiePropertyKeyEnum) { + FWFNSHttpCookiePropertyKeyEnumComment = 0, + FWFNSHttpCookiePropertyKeyEnumCommentUrl = 1, + FWFNSHttpCookiePropertyKeyEnumDiscard = 2, + FWFNSHttpCookiePropertyKeyEnumDomain = 3, + FWFNSHttpCookiePropertyKeyEnumExpires = 4, + FWFNSHttpCookiePropertyKeyEnumMaximumAge = 5, + FWFNSHttpCookiePropertyKeyEnumName = 6, + FWFNSHttpCookiePropertyKeyEnumOriginUrl = 7, + FWFNSHttpCookiePropertyKeyEnumPath = 8, + FWFNSHttpCookiePropertyKeyEnumPort = 9, + FWFNSHttpCookiePropertyKeyEnumSameSitePolicy = 10, + FWFNSHttpCookiePropertyKeyEnumSecure = 11, + FWFNSHttpCookiePropertyKeyEnumValue = 12, + FWFNSHttpCookiePropertyKeyEnumVersion = 13, +}; + +@class FWFNSKeyValueObservingOptionsEnumData; +@class FWFNSKeyValueChangeKeyEnumData; +@class FWFWKUserScriptInjectionTimeEnumData; +@class FWFWKAudiovisualMediaTypeEnumData; +@class FWFWKWebsiteDataTypeEnumData; +@class FWFWKNavigationActionPolicyEnumData; +@class FWFNSHttpCookiePropertyKeyEnumData; +@class FWFNSUrlRequestData; +@class FWFWKUserScriptData; +@class FWFWKNavigationActionData; +@class FWFWKFrameInfoData; +@class FWFNSErrorData; +@class FWFWKScriptMessageData; +@class FWFNSHttpCookieData; + +@interface FWFNSKeyValueObservingOptionsEnumData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FWFNSKeyValueObservingOptionsEnum)value; +@property(nonatomic, assign) FWFNSKeyValueObservingOptionsEnum value; +@end + +@interface FWFNSKeyValueChangeKeyEnumData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FWFNSKeyValueChangeKeyEnum)value; +@property(nonatomic, assign) FWFNSKeyValueChangeKeyEnum value; +@end + +@interface FWFWKUserScriptInjectionTimeEnumData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FWFWKUserScriptInjectionTimeEnum)value; +@property(nonatomic, assign) FWFWKUserScriptInjectionTimeEnum value; +@end + +@interface FWFWKAudiovisualMediaTypeEnumData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FWFWKAudiovisualMediaTypeEnum)value; +@property(nonatomic, assign) FWFWKAudiovisualMediaTypeEnum value; +@end + +@interface FWFWKWebsiteDataTypeEnumData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FWFWKWebsiteDataTypeEnum)value; +@property(nonatomic, assign) FWFWKWebsiteDataTypeEnum value; +@end + +@interface FWFWKNavigationActionPolicyEnumData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FWFWKNavigationActionPolicyEnum)value; +@property(nonatomic, assign) FWFWKNavigationActionPolicyEnum value; +@end + +@interface FWFNSHttpCookiePropertyKeyEnumData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FWFNSHttpCookiePropertyKeyEnum)value; +@property(nonatomic, assign) FWFNSHttpCookiePropertyKeyEnum value; +@end + +@interface FWFNSUrlRequestData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithUrl:(NSString *)url + httpMethod:(nullable NSString *)httpMethod + httpBody:(nullable FlutterStandardTypedData *)httpBody + allHttpHeaderFields:(NSDictionary *)allHttpHeaderFields; +@property(nonatomic, copy) NSString *url; +@property(nonatomic, copy, nullable) NSString *httpMethod; +@property(nonatomic, strong, nullable) FlutterStandardTypedData *httpBody; +@property(nonatomic, strong) NSDictionary *allHttpHeaderFields; +@end + +@interface FWFWKUserScriptData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithSource:(NSString *)source + injectionTime:(nullable FWFWKUserScriptInjectionTimeEnumData *)injectionTime + isMainFrameOnly:(NSNumber *)isMainFrameOnly; +@property(nonatomic, copy) NSString *source; +@property(nonatomic, strong, nullable) FWFWKUserScriptInjectionTimeEnumData *injectionTime; +@property(nonatomic, strong) NSNumber *isMainFrameOnly; +@end + +@interface FWFWKNavigationActionData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithRequest:(FWFNSUrlRequestData *)request + targetFrame:(FWFWKFrameInfoData *)targetFrame; +@property(nonatomic, strong) FWFNSUrlRequestData *request; +@property(nonatomic, strong) FWFWKFrameInfoData *targetFrame; +@end + +@interface FWFWKFrameInfoData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithIsMainFrame:(NSNumber *)isMainFrame; +@property(nonatomic, strong) NSNumber *isMainFrame; +@end + +@interface FWFNSErrorData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithCode:(NSNumber *)code + domain:(NSString *)domain + localizedDescription:(NSString *)localizedDescription; +@property(nonatomic, strong) NSNumber *code; +@property(nonatomic, copy) NSString *domain; +@property(nonatomic, copy) NSString *localizedDescription; +@end + +@interface FWFWKScriptMessageData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithName:(NSString *)name body:(id)body; +@property(nonatomic, copy) NSString *name; +@property(nonatomic, strong) id body; +@end + +@interface FWFNSHttpCookieData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithPropertyKeys:(NSArray *)propertyKeys + propertyValues:(NSArray *)propertyValues; +@property(nonatomic, strong) NSArray *propertyKeys; +@property(nonatomic, strong) NSArray *propertyValues; +@end + +/// The codec used by FWFWKWebsiteDataStoreHostApi. +NSObject *FWFWKWebsiteDataStoreHostApiGetCodec(void); + +@protocol FWFWKWebsiteDataStoreHostApi +- (void)createFromWebViewConfigurationWithIdentifier:(NSNumber *)identifier + configurationIdentifier:(NSNumber *)configurationIdentifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)createDefaultDataStoreWithIdentifier:(NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)removeDataFromDataStoreWithIdentifier:(NSNumber *)identifier + ofTypes:(NSArray *)dataTypes + modifiedSince:(NSNumber *)modificationTimeInSecondsSinceEpoch + completion:(void (^)(NSNumber *_Nullable, + FlutterError *_Nullable))completion; +@end + +extern void FWFWKWebsiteDataStoreHostApiSetup( + id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFUIViewHostApi. +NSObject *FWFUIViewHostApiGetCodec(void); + +@protocol FWFUIViewHostApi +- (void)setBackgroundColorForViewWithIdentifier:(NSNumber *)identifier + toValue:(nullable NSNumber *)value + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setOpaqueForViewWithIdentifier:(NSNumber *)identifier + isOpaque:(NSNumber *)opaque + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFUIViewHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFUIScrollViewHostApi. +NSObject *FWFUIScrollViewHostApiGetCodec(void); + +@protocol FWFUIScrollViewHostApi +- (void)createFromWebViewWithIdentifier:(NSNumber *)identifier + webViewIdentifier:(NSNumber *)webViewIdentifier + error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSArray *) + contentOffsetForScrollViewWithIdentifier:(NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)scrollByForScrollViewWithIdentifier:(NSNumber *)identifier + x:(NSNumber *)x + y:(NSNumber *)y + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setContentOffsetForScrollViewWithIdentifier:(NSNumber *)identifier + toX:(NSNumber *)x + y:(NSNumber *)y + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFUIScrollViewHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKWebViewConfigurationHostApi. +NSObject *FWFWKWebViewConfigurationHostApiGetCodec(void); + +@protocol FWFWKWebViewConfigurationHostApi +- (void)createWithIdentifier:(NSNumber *)identifier error:(FlutterError *_Nullable *_Nonnull)error; +- (void)createFromWebViewWithIdentifier:(NSNumber *)identifier + webViewIdentifier:(NSNumber *)webViewIdentifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:(NSNumber *)identifier + isAllowed:(NSNumber *)allow + error: + (FlutterError *_Nullable *_Nonnull) + error; +- (void) + setMediaTypesRequiresUserActionForConfigurationWithIdentifier:(NSNumber *)identifier + forTypes: + (NSArray< + FWFWKAudiovisualMediaTypeEnumData + *> *)types + error: + (FlutterError *_Nullable *_Nonnull) + error; +@end + +extern void FWFWKWebViewConfigurationHostApiSetup( + id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKWebViewConfigurationFlutterApi. +NSObject *FWFWKWebViewConfigurationFlutterApiGetCodec(void); + +@interface FWFWKWebViewConfigurationFlutterApi : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger; +- (void)createWithIdentifier:(NSNumber *)identifier + completion:(void (^)(NSError *_Nullable))completion; +@end +/// The codec used by FWFWKUserContentControllerHostApi. +NSObject *FWFWKUserContentControllerHostApiGetCodec(void); + +@protocol FWFWKUserContentControllerHostApi +- (void)createFromWebViewConfigurationWithIdentifier:(NSNumber *)identifier + configurationIdentifier:(NSNumber *)configurationIdentifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)addScriptMessageHandlerForControllerWithIdentifier:(NSNumber *)identifier + handlerIdentifier:(NSNumber *)handlerIdentifier + ofName:(NSString *)name + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)removeScriptMessageHandlerForControllerWithIdentifier:(NSNumber *)identifier + name:(NSString *)name + error:(FlutterError *_Nullable *_Nonnull) + error; +- (void)removeAllScriptMessageHandlersForControllerWithIdentifier:(NSNumber *)identifier + error: + (FlutterError *_Nullable *_Nonnull) + error; +- (void)addUserScriptForControllerWithIdentifier:(NSNumber *)identifier + userScript:(FWFWKUserScriptData *)userScript + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)removeAllUserScriptsForControllerWithIdentifier:(NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFWKUserContentControllerHostApiSetup( + id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKPreferencesHostApi. +NSObject *FWFWKPreferencesHostApiGetCodec(void); + +@protocol FWFWKPreferencesHostApi +- (void)createFromWebViewConfigurationWithIdentifier:(NSNumber *)identifier + configurationIdentifier:(NSNumber *)configurationIdentifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setJavaScriptEnabledForPreferencesWithIdentifier:(NSNumber *)identifier + isEnabled:(NSNumber *)enabled + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFWKPreferencesHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKScriptMessageHandlerHostApi. +NSObject *FWFWKScriptMessageHandlerHostApiGetCodec(void); + +@protocol FWFWKScriptMessageHandlerHostApi +- (void)createWithIdentifier:(NSNumber *)identifier error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFWKScriptMessageHandlerHostApiSetup( + id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKScriptMessageHandlerFlutterApi. +NSObject *FWFWKScriptMessageHandlerFlutterApiGetCodec(void); + +@interface FWFWKScriptMessageHandlerFlutterApi : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger; +- (void)didReceiveScriptMessageForHandlerWithIdentifier:(NSNumber *)identifier + userContentControllerIdentifier:(NSNumber *)userContentControllerIdentifier + message:(FWFWKScriptMessageData *)message + completion:(void (^)(NSError *_Nullable))completion; +@end +/// The codec used by FWFWKNavigationDelegateHostApi. +NSObject *FWFWKNavigationDelegateHostApiGetCodec(void); + +@protocol FWFWKNavigationDelegateHostApi +- (void)createWithIdentifier:(NSNumber *)identifier error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFWKNavigationDelegateHostApiSetup( + id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKNavigationDelegateFlutterApi. +NSObject *FWFWKNavigationDelegateFlutterApiGetCodec(void); + +@interface FWFWKNavigationDelegateFlutterApi : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger; +- (void)didFinishNavigationForDelegateWithIdentifier:(NSNumber *)identifier + webViewIdentifier:(NSNumber *)webViewIdentifier + URL:(nullable NSString *)url + completion:(void (^)(NSError *_Nullable))completion; +- (void)didStartProvisionalNavigationForDelegateWithIdentifier:(NSNumber *)identifier + webViewIdentifier:(NSNumber *)webViewIdentifier + URL:(nullable NSString *)url + completion: + (void (^)(NSError *_Nullable))completion; +- (void) + decidePolicyForNavigationActionForDelegateWithIdentifier:(NSNumber *)identifier + webViewIdentifier:(NSNumber *)webViewIdentifier + navigationAction: + (FWFWKNavigationActionData *)navigationAction + completion: + (void (^)(FWFWKNavigationActionPolicyEnumData + *_Nullable, + NSError *_Nullable))completion; +- (void)didFailNavigationForDelegateWithIdentifier:(NSNumber *)identifier + webViewIdentifier:(NSNumber *)webViewIdentifier + error:(FWFNSErrorData *)error + completion:(void (^)(NSError *_Nullable))completion; +- (void)didFailProvisionalNavigationForDelegateWithIdentifier:(NSNumber *)identifier + webViewIdentifier:(NSNumber *)webViewIdentifier + error:(FWFNSErrorData *)error + completion: + (void (^)(NSError *_Nullable))completion; +- (void)webViewWebContentProcessDidTerminateForDelegateWithIdentifier:(NSNumber *)identifier + webViewIdentifier:(NSNumber *)webViewIdentifier + completion:(void (^)(NSError *_Nullable)) + completion; +@end +/// The codec used by FWFNSObjectHostApi. +NSObject *FWFNSObjectHostApiGetCodec(void); + +@protocol FWFNSObjectHostApi +- (void)disposeObjectWithIdentifier:(NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)addObserverForObjectWithIdentifier:(NSNumber *)identifier + observerIdentifier:(NSNumber *)observerIdentifier + keyPath:(NSString *)keyPath + options: + (NSArray *)options + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)removeObserverForObjectWithIdentifier:(NSNumber *)identifier + observerIdentifier:(NSNumber *)observerIdentifier + keyPath:(NSString *)keyPath + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFNSObjectHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFNSObjectFlutterApi. +NSObject *FWFNSObjectFlutterApiGetCodec(void); + +@interface FWFNSObjectFlutterApi : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger; +- (void)observeValueForObjectWithIdentifier:(NSNumber *)identifier + keyPath:(NSString *)keyPath + objectIdentifier:(NSNumber *)objectIdentifier + changeKeys:(NSArray *)changeKeys + changeValues:(NSArray *)changeValues + completion:(void (^)(NSError *_Nullable))completion; +- (void)disposeObjectWithIdentifier:(NSNumber *)identifier + completion:(void (^)(NSError *_Nullable))completion; +@end +/// The codec used by FWFWKWebViewHostApi. +NSObject *FWFWKWebViewHostApiGetCodec(void); + +@protocol FWFWKWebViewHostApi +- (void)createWithIdentifier:(NSNumber *)identifier + configurationIdentifier:(NSNumber *)configurationIdentifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setUIDelegateForWebViewWithIdentifier:(NSNumber *)identifier + delegateIdentifier:(nullable NSNumber *)uiDelegateIdentifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setNavigationDelegateForWebViewWithIdentifier:(NSNumber *)identifier + delegateIdentifier: + (nullable NSNumber *)navigationDelegateIdentifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSString *)URLForWebViewWithIdentifier:(NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)estimatedProgressForWebViewWithIdentifier:(NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull) + error; +- (void)loadRequestForWebViewWithIdentifier:(NSNumber *)identifier + request:(FWFNSUrlRequestData *)request + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)loadHTMLForWebViewWithIdentifier:(NSNumber *)identifier + HTMLString:(NSString *)string + baseURL:(nullable NSString *)baseUrl + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)loadFileForWebViewWithIdentifier:(NSNumber *)identifier + fileURL:(NSString *)url + readAccessURL:(NSString *)readAccessUrl + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)loadAssetForWebViewWithIdentifier:(NSNumber *)identifier + assetKey:(NSString *)key + error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)canGoBackForWebViewWithIdentifier:(NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)canGoForwardForWebViewWithIdentifier:(NSNumber *)identifier + error: + (FlutterError *_Nullable *_Nonnull)error; +- (void)goBackForWebViewWithIdentifier:(NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)goForwardForWebViewWithIdentifier:(NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)reloadWebViewWithIdentifier:(NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSString *)titleForWebViewWithIdentifier:(NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setAllowsBackForwardForWebViewWithIdentifier:(NSNumber *)identifier + isAllowed:(NSNumber *)allow + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setUserAgentForWebViewWithIdentifier:(NSNumber *)identifier + userAgent:(nullable NSString *)userAgent + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)evaluateJavaScriptForWebViewWithIdentifier:(NSNumber *)identifier + javaScriptString:(NSString *)javaScriptString + completion:(void (^)(id _Nullable, + FlutterError *_Nullable))completion; +@end + +extern void FWFWKWebViewHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKUIDelegateHostApi. +NSObject *FWFWKUIDelegateHostApiGetCodec(void); + +@protocol FWFWKUIDelegateHostApi +- (void)createWithIdentifier:(NSNumber *)identifier error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void FWFWKUIDelegateHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +/// The codec used by FWFWKUIDelegateFlutterApi. +NSObject *FWFWKUIDelegateFlutterApiGetCodec(void); + +@interface FWFWKUIDelegateFlutterApi : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger; +- (void)onCreateWebViewForDelegateWithIdentifier:(NSNumber *)identifier + webViewIdentifier:(NSNumber *)webViewIdentifier + configurationIdentifier:(NSNumber *)configurationIdentifier + navigationAction:(FWFWKNavigationActionData *)navigationAction + completion:(void (^)(NSError *_Nullable))completion; +@end +/// The codec used by FWFWKHttpCookieStoreHostApi. +NSObject *FWFWKHttpCookieStoreHostApiGetCodec(void); + +@protocol FWFWKHttpCookieStoreHostApi +- (void)createFromWebsiteDataStoreWithIdentifier:(NSNumber *)identifier + dataStoreIdentifier:(NSNumber *)websiteDataStoreIdentifier + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setCookieForStoreWithIdentifier:(NSNumber *)identifier + cookie:(FWFNSHttpCookieData *)cookie + completion:(void (^)(FlutterError *_Nullable))completion; +@end + +extern void FWFWKHttpCookieStoreHostApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m new file mode 100644 index 000000000000..10680227ee43 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m @@ -0,0 +1,2813 @@ +// 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.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import "FWFGeneratedWebKitApis.h" +#import + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSDictionary *wrapResult(id result, FlutterError *error) { + NSDictionary *errorDict = (NSDictionary *)[NSNull null]; + if (error) { + errorDict = @{ + @"code" : (error.code ?: [NSNull null]), + @"message" : (error.message ?: [NSNull null]), + @"details" : (error.details ?: [NSNull null]), + }; + } + return @{ + @"result" : (result ?: [NSNull null]), + @"error" : errorDict, + }; +} +static id GetNullableObject(NSDictionary *dict, id key) { + id result = dict[key]; + return (result == [NSNull null]) ? nil : result; +} +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + +@interface FWFNSKeyValueObservingOptionsEnumData () ++ (FWFNSKeyValueObservingOptionsEnumData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFNSKeyValueObservingOptionsEnumData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFNSKeyValueChangeKeyEnumData () ++ (FWFNSKeyValueChangeKeyEnumData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFNSKeyValueChangeKeyEnumData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFWKUserScriptInjectionTimeEnumData () ++ (FWFWKUserScriptInjectionTimeEnumData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFWKUserScriptInjectionTimeEnumData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFWKAudiovisualMediaTypeEnumData () ++ (FWFWKAudiovisualMediaTypeEnumData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFWKAudiovisualMediaTypeEnumData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFWKWebsiteDataTypeEnumData () ++ (FWFWKWebsiteDataTypeEnumData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFWKWebsiteDataTypeEnumData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFWKNavigationActionPolicyEnumData () ++ (FWFWKNavigationActionPolicyEnumData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFWKNavigationActionPolicyEnumData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFNSHttpCookiePropertyKeyEnumData () ++ (FWFNSHttpCookiePropertyKeyEnumData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFNSHttpCookiePropertyKeyEnumData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFNSUrlRequestData () ++ (FWFNSUrlRequestData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFNSUrlRequestData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFWKUserScriptData () ++ (FWFWKUserScriptData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFWKUserScriptData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFWKNavigationActionData () ++ (FWFWKNavigationActionData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFWKNavigationActionData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFWKFrameInfoData () ++ (FWFWKFrameInfoData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFWKFrameInfoData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFNSErrorData () ++ (FWFNSErrorData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFNSErrorData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFWKScriptMessageData () ++ (FWFWKScriptMessageData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFWKScriptMessageData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FWFNSHttpCookieData () ++ (FWFNSHttpCookieData *)fromMap:(NSDictionary *)dict; ++ (nullable FWFNSHttpCookieData *)nullableFromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end + +@implementation FWFNSKeyValueObservingOptionsEnumData ++ (instancetype)makeWithValue:(FWFNSKeyValueObservingOptionsEnum)value { + FWFNSKeyValueObservingOptionsEnumData *pigeonResult = + [[FWFNSKeyValueObservingOptionsEnumData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFNSKeyValueObservingOptionsEnumData *)fromMap:(NSDictionary *)dict { + FWFNSKeyValueObservingOptionsEnumData *pigeonResult = + [[FWFNSKeyValueObservingOptionsEnumData alloc] init]; + pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + return pigeonResult; +} ++ (nullable FWFNSKeyValueObservingOptionsEnumData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFNSKeyValueObservingOptionsEnumData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"value" : @(self.value), + }; +} +@end + +@implementation FWFNSKeyValueChangeKeyEnumData ++ (instancetype)makeWithValue:(FWFNSKeyValueChangeKeyEnum)value { + FWFNSKeyValueChangeKeyEnumData *pigeonResult = [[FWFNSKeyValueChangeKeyEnumData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFNSKeyValueChangeKeyEnumData *)fromMap:(NSDictionary *)dict { + FWFNSKeyValueChangeKeyEnumData *pigeonResult = [[FWFNSKeyValueChangeKeyEnumData alloc] init]; + pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + return pigeonResult; +} ++ (nullable FWFNSKeyValueChangeKeyEnumData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFNSKeyValueChangeKeyEnumData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"value" : @(self.value), + }; +} +@end + +@implementation FWFWKUserScriptInjectionTimeEnumData ++ (instancetype)makeWithValue:(FWFWKUserScriptInjectionTimeEnum)value { + FWFWKUserScriptInjectionTimeEnumData *pigeonResult = + [[FWFWKUserScriptInjectionTimeEnumData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFWKUserScriptInjectionTimeEnumData *)fromMap:(NSDictionary *)dict { + FWFWKUserScriptInjectionTimeEnumData *pigeonResult = + [[FWFWKUserScriptInjectionTimeEnumData alloc] init]; + pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + return pigeonResult; +} ++ (nullable FWFWKUserScriptInjectionTimeEnumData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFWKUserScriptInjectionTimeEnumData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"value" : @(self.value), + }; +} +@end + +@implementation FWFWKAudiovisualMediaTypeEnumData ++ (instancetype)makeWithValue:(FWFWKAudiovisualMediaTypeEnum)value { + FWFWKAudiovisualMediaTypeEnumData *pigeonResult = + [[FWFWKAudiovisualMediaTypeEnumData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFWKAudiovisualMediaTypeEnumData *)fromMap:(NSDictionary *)dict { + FWFWKAudiovisualMediaTypeEnumData *pigeonResult = + [[FWFWKAudiovisualMediaTypeEnumData alloc] init]; + pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + return pigeonResult; +} ++ (nullable FWFWKAudiovisualMediaTypeEnumData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFWKAudiovisualMediaTypeEnumData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"value" : @(self.value), + }; +} +@end + +@implementation FWFWKWebsiteDataTypeEnumData ++ (instancetype)makeWithValue:(FWFWKWebsiteDataTypeEnum)value { + FWFWKWebsiteDataTypeEnumData *pigeonResult = [[FWFWKWebsiteDataTypeEnumData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFWKWebsiteDataTypeEnumData *)fromMap:(NSDictionary *)dict { + FWFWKWebsiteDataTypeEnumData *pigeonResult = [[FWFWKWebsiteDataTypeEnumData alloc] init]; + pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + return pigeonResult; +} ++ (nullable FWFWKWebsiteDataTypeEnumData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFWKWebsiteDataTypeEnumData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"value" : @(self.value), + }; +} +@end + +@implementation FWFWKNavigationActionPolicyEnumData ++ (instancetype)makeWithValue:(FWFWKNavigationActionPolicyEnum)value { + FWFWKNavigationActionPolicyEnumData *pigeonResult = + [[FWFWKNavigationActionPolicyEnumData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFWKNavigationActionPolicyEnumData *)fromMap:(NSDictionary *)dict { + FWFWKNavigationActionPolicyEnumData *pigeonResult = + [[FWFWKNavigationActionPolicyEnumData alloc] init]; + pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + return pigeonResult; +} ++ (nullable FWFWKNavigationActionPolicyEnumData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFWKNavigationActionPolicyEnumData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"value" : @(self.value), + }; +} +@end + +@implementation FWFNSHttpCookiePropertyKeyEnumData ++ (instancetype)makeWithValue:(FWFNSHttpCookiePropertyKeyEnum)value { + FWFNSHttpCookiePropertyKeyEnumData *pigeonResult = + [[FWFNSHttpCookiePropertyKeyEnumData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFNSHttpCookiePropertyKeyEnumData *)fromMap:(NSDictionary *)dict { + FWFNSHttpCookiePropertyKeyEnumData *pigeonResult = + [[FWFNSHttpCookiePropertyKeyEnumData alloc] init]; + pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + return pigeonResult; +} ++ (nullable FWFNSHttpCookiePropertyKeyEnumData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFNSHttpCookiePropertyKeyEnumData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"value" : @(self.value), + }; +} +@end + +@implementation FWFNSUrlRequestData ++ (instancetype)makeWithUrl:(NSString *)url + httpMethod:(nullable NSString *)httpMethod + httpBody:(nullable FlutterStandardTypedData *)httpBody + allHttpHeaderFields:(NSDictionary *)allHttpHeaderFields { + FWFNSUrlRequestData *pigeonResult = [[FWFNSUrlRequestData alloc] init]; + pigeonResult.url = url; + pigeonResult.httpMethod = httpMethod; + pigeonResult.httpBody = httpBody; + pigeonResult.allHttpHeaderFields = allHttpHeaderFields; + return pigeonResult; +} ++ (FWFNSUrlRequestData *)fromMap:(NSDictionary *)dict { + FWFNSUrlRequestData *pigeonResult = [[FWFNSUrlRequestData alloc] init]; + pigeonResult.url = GetNullableObject(dict, @"url"); + NSAssert(pigeonResult.url != nil, @""); + pigeonResult.httpMethod = GetNullableObject(dict, @"httpMethod"); + pigeonResult.httpBody = GetNullableObject(dict, @"httpBody"); + pigeonResult.allHttpHeaderFields = GetNullableObject(dict, @"allHttpHeaderFields"); + NSAssert(pigeonResult.allHttpHeaderFields != nil, @""); + return pigeonResult; +} ++ (nullable FWFNSUrlRequestData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFNSUrlRequestData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"url" : (self.url ?: [NSNull null]), + @"httpMethod" : (self.httpMethod ?: [NSNull null]), + @"httpBody" : (self.httpBody ?: [NSNull null]), + @"allHttpHeaderFields" : (self.allHttpHeaderFields ?: [NSNull null]), + }; +} +@end + +@implementation FWFWKUserScriptData ++ (instancetype)makeWithSource:(NSString *)source + injectionTime:(nullable FWFWKUserScriptInjectionTimeEnumData *)injectionTime + isMainFrameOnly:(NSNumber *)isMainFrameOnly { + FWFWKUserScriptData *pigeonResult = [[FWFWKUserScriptData alloc] init]; + pigeonResult.source = source; + pigeonResult.injectionTime = injectionTime; + pigeonResult.isMainFrameOnly = isMainFrameOnly; + return pigeonResult; +} ++ (FWFWKUserScriptData *)fromMap:(NSDictionary *)dict { + FWFWKUserScriptData *pigeonResult = [[FWFWKUserScriptData alloc] init]; + pigeonResult.source = GetNullableObject(dict, @"source"); + NSAssert(pigeonResult.source != nil, @""); + pigeonResult.injectionTime = [FWFWKUserScriptInjectionTimeEnumData + nullableFromMap:GetNullableObject(dict, @"injectionTime")]; + pigeonResult.isMainFrameOnly = GetNullableObject(dict, @"isMainFrameOnly"); + NSAssert(pigeonResult.isMainFrameOnly != nil, @""); + return pigeonResult; +} ++ (nullable FWFWKUserScriptData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFWKUserScriptData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"source" : (self.source ?: [NSNull null]), + @"injectionTime" : (self.injectionTime ? [self.injectionTime toMap] : [NSNull null]), + @"isMainFrameOnly" : (self.isMainFrameOnly ?: [NSNull null]), + }; +} +@end + +@implementation FWFWKNavigationActionData ++ (instancetype)makeWithRequest:(FWFNSUrlRequestData *)request + targetFrame:(FWFWKFrameInfoData *)targetFrame { + FWFWKNavigationActionData *pigeonResult = [[FWFWKNavigationActionData alloc] init]; + pigeonResult.request = request; + pigeonResult.targetFrame = targetFrame; + return pigeonResult; +} ++ (FWFWKNavigationActionData *)fromMap:(NSDictionary *)dict { + FWFWKNavigationActionData *pigeonResult = [[FWFWKNavigationActionData alloc] init]; + pigeonResult.request = [FWFNSUrlRequestData nullableFromMap:GetNullableObject(dict, @"request")]; + NSAssert(pigeonResult.request != nil, @""); + pigeonResult.targetFrame = + [FWFWKFrameInfoData nullableFromMap:GetNullableObject(dict, @"targetFrame")]; + NSAssert(pigeonResult.targetFrame != nil, @""); + return pigeonResult; +} ++ (nullable FWFWKNavigationActionData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFWKNavigationActionData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"request" : (self.request ? [self.request toMap] : [NSNull null]), + @"targetFrame" : (self.targetFrame ? [self.targetFrame toMap] : [NSNull null]), + }; +} +@end + +@implementation FWFWKFrameInfoData ++ (instancetype)makeWithIsMainFrame:(NSNumber *)isMainFrame { + FWFWKFrameInfoData *pigeonResult = [[FWFWKFrameInfoData alloc] init]; + pigeonResult.isMainFrame = isMainFrame; + return pigeonResult; +} ++ (FWFWKFrameInfoData *)fromMap:(NSDictionary *)dict { + FWFWKFrameInfoData *pigeonResult = [[FWFWKFrameInfoData alloc] init]; + pigeonResult.isMainFrame = GetNullableObject(dict, @"isMainFrame"); + NSAssert(pigeonResult.isMainFrame != nil, @""); + return pigeonResult; +} ++ (nullable FWFWKFrameInfoData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFWKFrameInfoData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"isMainFrame" : (self.isMainFrame ?: [NSNull null]), + }; +} +@end + +@implementation FWFNSErrorData ++ (instancetype)makeWithCode:(NSNumber *)code + domain:(NSString *)domain + localizedDescription:(NSString *)localizedDescription { + FWFNSErrorData *pigeonResult = [[FWFNSErrorData alloc] init]; + pigeonResult.code = code; + pigeonResult.domain = domain; + pigeonResult.localizedDescription = localizedDescription; + return pigeonResult; +} ++ (FWFNSErrorData *)fromMap:(NSDictionary *)dict { + FWFNSErrorData *pigeonResult = [[FWFNSErrorData alloc] init]; + pigeonResult.code = GetNullableObject(dict, @"code"); + NSAssert(pigeonResult.code != nil, @""); + pigeonResult.domain = GetNullableObject(dict, @"domain"); + NSAssert(pigeonResult.domain != nil, @""); + pigeonResult.localizedDescription = GetNullableObject(dict, @"localizedDescription"); + NSAssert(pigeonResult.localizedDescription != nil, @""); + return pigeonResult; +} ++ (nullable FWFNSErrorData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFNSErrorData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"code" : (self.code ?: [NSNull null]), + @"domain" : (self.domain ?: [NSNull null]), + @"localizedDescription" : (self.localizedDescription ?: [NSNull null]), + }; +} +@end + +@implementation FWFWKScriptMessageData ++ (instancetype)makeWithName:(NSString *)name body:(id)body { + FWFWKScriptMessageData *pigeonResult = [[FWFWKScriptMessageData alloc] init]; + pigeonResult.name = name; + pigeonResult.body = body; + return pigeonResult; +} ++ (FWFWKScriptMessageData *)fromMap:(NSDictionary *)dict { + FWFWKScriptMessageData *pigeonResult = [[FWFWKScriptMessageData alloc] init]; + pigeonResult.name = GetNullableObject(dict, @"name"); + NSAssert(pigeonResult.name != nil, @""); + pigeonResult.body = GetNullableObject(dict, @"body"); + return pigeonResult; +} ++ (nullable FWFWKScriptMessageData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFWKScriptMessageData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"name" : (self.name ?: [NSNull null]), + @"body" : (self.body ?: [NSNull null]), + }; +} +@end + +@implementation FWFNSHttpCookieData ++ (instancetype)makeWithPropertyKeys:(NSArray *)propertyKeys + propertyValues:(NSArray *)propertyValues { + FWFNSHttpCookieData *pigeonResult = [[FWFNSHttpCookieData alloc] init]; + pigeonResult.propertyKeys = propertyKeys; + pigeonResult.propertyValues = propertyValues; + return pigeonResult; +} ++ (FWFNSHttpCookieData *)fromMap:(NSDictionary *)dict { + FWFNSHttpCookieData *pigeonResult = [[FWFNSHttpCookieData alloc] init]; + pigeonResult.propertyKeys = GetNullableObject(dict, @"propertyKeys"); + NSAssert(pigeonResult.propertyKeys != nil, @""); + pigeonResult.propertyValues = GetNullableObject(dict, @"propertyValues"); + NSAssert(pigeonResult.propertyValues != nil, @""); + return pigeonResult; +} ++ (nullable FWFNSHttpCookieData *)nullableFromMap:(NSDictionary *)dict { + return (dict) ? [FWFNSHttpCookieData fromMap:dict] : nil; +} +- (NSDictionary *)toMap { + return @{ + @"propertyKeys" : (self.propertyKeys ?: [NSNull null]), + @"propertyValues" : (self.propertyValues ?: [NSNull null]), + }; +} +@end + +@interface FWFWKWebsiteDataStoreHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKWebsiteDataStoreHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFWKWebsiteDataTypeEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFWKWebsiteDataStoreHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKWebsiteDataStoreHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFWKWebsiteDataTypeEnumData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFWKWebsiteDataStoreHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKWebsiteDataStoreHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKWebsiteDataStoreHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKWebsiteDataStoreHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKWebsiteDataStoreHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKWebsiteDataStoreHostApiCodecReaderWriter *readerWriter = + [[FWFWKWebsiteDataStoreHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKWebsiteDataStoreHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration" + binaryMessenger:binaryMessenger + codec:FWFWKWebsiteDataStoreHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(createFromWebViewConfigurationWithIdentifier: + configurationIdentifier:error:)], + @"FWFWKWebsiteDataStoreHostApi api (%@) doesn't respond to " + @"@selector(createFromWebViewConfigurationWithIdentifier:configurationIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_configurationIdentifier = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createFromWebViewConfigurationWithIdentifier:arg_identifier + configurationIdentifier:arg_configurationIdentifier + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createDefaultDataStore" + binaryMessenger:binaryMessenger + codec:FWFWKWebsiteDataStoreHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createDefaultDataStoreWithIdentifier:error:)], + @"FWFWKWebsiteDataStoreHostApi api (%@) doesn't respond to " + @"@selector(createDefaultDataStoreWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api createDefaultDataStoreWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes" + binaryMessenger:binaryMessenger + codec:FWFWKWebsiteDataStoreHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector + (removeDataFromDataStoreWithIdentifier:ofTypes:modifiedSince:completion:)], + @"FWFWKWebsiteDataStoreHostApi api (%@) doesn't respond to " + @"@selector(removeDataFromDataStoreWithIdentifier:ofTypes:modifiedSince:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSArray *arg_dataTypes = GetNullableObjectAtIndex(args, 1); + NSNumber *arg_modificationTimeInSecondsSinceEpoch = GetNullableObjectAtIndex(args, 2); + [api removeDataFromDataStoreWithIdentifier:arg_identifier + ofTypes:arg_dataTypes + modifiedSince:arg_modificationTimeInSecondsSinceEpoch + completion:^(NSNumber *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFUIViewHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFUIViewHostApiCodecReader +@end + +@interface FWFUIViewHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFUIViewHostApiCodecWriter +@end + +@interface FWFUIViewHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFUIViewHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFUIViewHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFUIViewHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFUIViewHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFUIViewHostApiCodecReaderWriter *readerWriter = + [[FWFUIViewHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFUIViewHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.UIViewHostApi.setBackgroundColor" + binaryMessenger:binaryMessenger + codec:FWFUIViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setBackgroundColorForViewWithIdentifier: + toValue:error:)], + @"FWFUIViewHostApi api (%@) doesn't respond to " + @"@selector(setBackgroundColorForViewWithIdentifier:toValue:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_value = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setBackgroundColorForViewWithIdentifier:arg_identifier toValue:arg_value error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.UIViewHostApi.setOpaque" + binaryMessenger:binaryMessenger + codec:FWFUIViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setOpaqueForViewWithIdentifier:isOpaque:error:)], + @"FWFUIViewHostApi api (%@) doesn't respond to " + @"@selector(setOpaqueForViewWithIdentifier:isOpaque:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_opaque = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setOpaqueForViewWithIdentifier:arg_identifier isOpaque:arg_opaque error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFUIScrollViewHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFUIScrollViewHostApiCodecReader +@end + +@interface FWFUIScrollViewHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFUIScrollViewHostApiCodecWriter +@end + +@interface FWFUIScrollViewHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFUIScrollViewHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFUIScrollViewHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFUIScrollViewHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFUIScrollViewHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFUIScrollViewHostApiCodecReaderWriter *readerWriter = + [[FWFUIScrollViewHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFUIScrollViewHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.UIScrollViewHostApi.createFromWebView" + binaryMessenger:binaryMessenger + codec:FWFUIScrollViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createFromWebViewWithIdentifier: + webViewIdentifier:error:)], + @"FWFUIScrollViewHostApi api (%@) doesn't respond to " + @"@selector(createFromWebViewWithIdentifier:webViewIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_webViewIdentifier = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createFromWebViewWithIdentifier:arg_identifier + webViewIdentifier:arg_webViewIdentifier + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.UIScrollViewHostApi.getContentOffset" + binaryMessenger:binaryMessenger + codec:FWFUIScrollViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(contentOffsetForScrollViewWithIdentifier:error:)], + @"FWFUIScrollViewHostApi api (%@) doesn't respond to " + @"@selector(contentOffsetForScrollViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + NSArray *output = [api contentOffsetForScrollViewWithIdentifier:arg_identifier + error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.UIScrollViewHostApi.scrollBy" + binaryMessenger:binaryMessenger + codec:FWFUIScrollViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(scrollByForScrollViewWithIdentifier:x:y:error:)], + @"FWFUIScrollViewHostApi api (%@) doesn't respond to " + @"@selector(scrollByForScrollViewWithIdentifier:x:y:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_x = GetNullableObjectAtIndex(args, 1); + NSNumber *arg_y = GetNullableObjectAtIndex(args, 2); + FlutterError *error; + [api scrollByForScrollViewWithIdentifier:arg_identifier x:arg_x y:arg_y error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset" + binaryMessenger:binaryMessenger + codec:FWFUIScrollViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (setContentOffsetForScrollViewWithIdentifier:toX:y:error:)], + @"FWFUIScrollViewHostApi api (%@) doesn't respond to " + @"@selector(setContentOffsetForScrollViewWithIdentifier:toX:y:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_x = GetNullableObjectAtIndex(args, 1); + NSNumber *arg_y = GetNullableObjectAtIndex(args, 2); + FlutterError *error; + [api setContentOffsetForScrollViewWithIdentifier:arg_identifier + toX:arg_x + y:arg_y + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKWebViewConfigurationHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKWebViewConfigurationHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFWKAudiovisualMediaTypeEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFWKWebViewConfigurationHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKWebViewConfigurationHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFWKAudiovisualMediaTypeEnumData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFWKWebViewConfigurationHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKWebViewConfigurationHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKWebViewConfigurationHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKWebViewConfigurationHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKWebViewConfigurationHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKWebViewConfigurationHostApiCodecReaderWriter *readerWriter = + [[FWFWKWebViewConfigurationHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKWebViewConfigurationHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewConfigurationHostApi.create" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewConfigurationHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createWithIdentifier:error:)], + @"FWFWKWebViewConfigurationHostApi api (%@) doesn't respond to " + @"@selector(createWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api createWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewConfigurationHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createFromWebViewWithIdentifier: + webViewIdentifier:error:)], + @"FWFWKWebViewConfigurationHostApi api (%@) doesn't respond to " + @"@selector(createFromWebViewWithIdentifier:webViewIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_webViewIdentifier = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createFromWebViewWithIdentifier:arg_identifier + webViewIdentifier:arg_webViewIdentifier + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewConfigurationHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector + (setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:isAllowed:error:)], + @"FWFWKWebViewConfigurationHostApi api (%@) doesn't respond to " + @"@selector(setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:isAllowed:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_allow = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:arg_identifier + isAllowed:arg_allow + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewConfigurationHostApi." + @"setMediaTypesRequiringUserActionForPlayback" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewConfigurationHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (setMediaTypesRequiresUserActionForConfigurationWithIdentifier: + forTypes:error:)], + @"FWFWKWebViewConfigurationHostApi api (%@) doesn't respond to " + @"@selector(setMediaTypesRequiresUserActionForConfigurationWithIdentifier:forTypes:" + @"error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSArray *arg_types = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setMediaTypesRequiresUserActionForConfigurationWithIdentifier:arg_identifier + forTypes:arg_types + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKWebViewConfigurationFlutterApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKWebViewConfigurationFlutterApiCodecReader +@end + +@interface FWFWKWebViewConfigurationFlutterApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKWebViewConfigurationFlutterApiCodecWriter +@end + +@interface FWFWKWebViewConfigurationFlutterApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKWebViewConfigurationFlutterApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKWebViewConfigurationFlutterApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKWebViewConfigurationFlutterApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKWebViewConfigurationFlutterApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKWebViewConfigurationFlutterApiCodecReaderWriter *readerWriter = + [[FWFWKWebViewConfigurationFlutterApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +@interface FWFWKWebViewConfigurationFlutterApi () +@property(nonatomic, strong) NSObject *binaryMessenger; +@end + +@implementation FWFWKWebViewConfigurationFlutterApi + +- (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { + self = [super init]; + if (self) { + _binaryMessenger = binaryMessenger; + } + return self; +} +- (void)createWithIdentifier:(NSNumber *)arg_identifier + completion:(void (^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.WKWebViewConfigurationFlutterApi.create" + binaryMessenger:self.binaryMessenger + codec:FWFWKWebViewConfigurationFlutterApiGetCodec()]; + [channel sendMessage:@[ arg_identifier ?: [NSNull null] ] + reply:^(id reply) { + completion(nil); + }]; +} +@end +@interface FWFWKUserContentControllerHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKUserContentControllerHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFWKUserScriptData fromMap:[self readValue]]; + + case 129: + return [FWFWKUserScriptInjectionTimeEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFWKUserContentControllerHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKUserContentControllerHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFWKUserScriptData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFWKUserContentControllerHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKUserContentControllerHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKUserContentControllerHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKUserContentControllerHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKUserContentControllerHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKUserContentControllerHostApiCodecReaderWriter *readerWriter = + [[FWFWKUserContentControllerHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKUserContentControllerHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.WKUserContentControllerHostApi.createFromWebViewConfiguration" + binaryMessenger:binaryMessenger + codec:FWFWKUserContentControllerHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(createFromWebViewConfigurationWithIdentifier: + configurationIdentifier:error:)], + @"FWFWKUserContentControllerHostApi api (%@) doesn't respond to " + @"@selector(createFromWebViewConfigurationWithIdentifier:configurationIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_configurationIdentifier = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createFromWebViewConfigurationWithIdentifier:arg_identifier + configurationIdentifier:arg_configurationIdentifier + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler" + binaryMessenger:binaryMessenger + codec:FWFWKUserContentControllerHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (addScriptMessageHandlerForControllerWithIdentifier: + handlerIdentifier:ofName:error:)], + @"FWFWKUserContentControllerHostApi api (%@) doesn't respond to " + @"@selector(addScriptMessageHandlerForControllerWithIdentifier:handlerIdentifier:" + @"ofName:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_handlerIdentifier = GetNullableObjectAtIndex(args, 1); + NSString *arg_name = GetNullableObjectAtIndex(args, 2); + FlutterError *error; + [api addScriptMessageHandlerForControllerWithIdentifier:arg_identifier + handlerIdentifier:arg_handlerIdentifier + ofName:arg_name + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.WKUserContentControllerHostApi.removeScriptMessageHandler" + binaryMessenger:binaryMessenger + codec:FWFWKUserContentControllerHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (removeScriptMessageHandlerForControllerWithIdentifier:name:error:)], + @"FWFWKUserContentControllerHostApi api (%@) doesn't respond to " + @"@selector(removeScriptMessageHandlerForControllerWithIdentifier:name:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSString *arg_name = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api removeScriptMessageHandlerForControllerWithIdentifier:arg_identifier + name:arg_name + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllScriptMessageHandlers" + binaryMessenger:binaryMessenger + codec:FWFWKUserContentControllerHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (removeAllScriptMessageHandlersForControllerWithIdentifier:error:)], + @"FWFWKUserContentControllerHostApi api (%@) doesn't respond to " + @"@selector(removeAllScriptMessageHandlersForControllerWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api removeAllScriptMessageHandlersForControllerWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKUserContentControllerHostApi.addUserScript" + binaryMessenger:binaryMessenger + codec:FWFWKUserContentControllerHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(addUserScriptForControllerWithIdentifier: + userScript:error:)], + @"FWFWKUserContentControllerHostApi api (%@) doesn't respond to " + @"@selector(addUserScriptForControllerWithIdentifier:userScript:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FWFWKUserScriptData *arg_userScript = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api addUserScriptForControllerWithIdentifier:arg_identifier + userScript:arg_userScript + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllUserScripts" + binaryMessenger:binaryMessenger + codec:FWFWKUserContentControllerHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (removeAllUserScriptsForControllerWithIdentifier:error:)], + @"FWFWKUserContentControllerHostApi api (%@) doesn't respond to " + @"@selector(removeAllUserScriptsForControllerWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api removeAllUserScriptsForControllerWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKPreferencesHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKPreferencesHostApiCodecReader +@end + +@interface FWFWKPreferencesHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKPreferencesHostApiCodecWriter +@end + +@interface FWFWKPreferencesHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKPreferencesHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKPreferencesHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKPreferencesHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKPreferencesHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKPreferencesHostApiCodecReaderWriter *readerWriter = + [[FWFWKPreferencesHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKPreferencesHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKPreferencesHostApi.createFromWebViewConfiguration" + binaryMessenger:binaryMessenger + codec:FWFWKPreferencesHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(createFromWebViewConfigurationWithIdentifier: + configurationIdentifier:error:)], + @"FWFWKPreferencesHostApi api (%@) doesn't respond to " + @"@selector(createFromWebViewConfigurationWithIdentifier:configurationIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_configurationIdentifier = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createFromWebViewConfigurationWithIdentifier:arg_identifier + configurationIdentifier:arg_configurationIdentifier + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKPreferencesHostApi.setJavaScriptEnabled" + binaryMessenger:binaryMessenger + codec:FWFWKPreferencesHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (setJavaScriptEnabledForPreferencesWithIdentifier:isEnabled:error:)], + @"FWFWKPreferencesHostApi api (%@) doesn't respond to " + @"@selector(setJavaScriptEnabledForPreferencesWithIdentifier:isEnabled:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_enabled = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setJavaScriptEnabledForPreferencesWithIdentifier:arg_identifier + isEnabled:arg_enabled + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKScriptMessageHandlerHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKScriptMessageHandlerHostApiCodecReader +@end + +@interface FWFWKScriptMessageHandlerHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKScriptMessageHandlerHostApiCodecWriter +@end + +@interface FWFWKScriptMessageHandlerHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKScriptMessageHandlerHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKScriptMessageHandlerHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKScriptMessageHandlerHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKScriptMessageHandlerHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKScriptMessageHandlerHostApiCodecReaderWriter *readerWriter = + [[FWFWKScriptMessageHandlerHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKScriptMessageHandlerHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKScriptMessageHandlerHostApi.create" + binaryMessenger:binaryMessenger + codec:FWFWKScriptMessageHandlerHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createWithIdentifier:error:)], + @"FWFWKScriptMessageHandlerHostApi api (%@) doesn't respond to " + @"@selector(createWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api createWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKScriptMessageHandlerFlutterApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKScriptMessageHandlerFlutterApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFWKScriptMessageData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFWKScriptMessageHandlerFlutterApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKScriptMessageHandlerFlutterApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFWKScriptMessageData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFWKScriptMessageHandlerFlutterApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKScriptMessageHandlerFlutterApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKScriptMessageHandlerFlutterApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKScriptMessageHandlerFlutterApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKScriptMessageHandlerFlutterApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKScriptMessageHandlerFlutterApiCodecReaderWriter *readerWriter = + [[FWFWKScriptMessageHandlerFlutterApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +@interface FWFWKScriptMessageHandlerFlutterApi () +@property(nonatomic, strong) NSObject *binaryMessenger; +@end + +@implementation FWFWKScriptMessageHandlerFlutterApi + +- (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { + self = [super init]; + if (self) { + _binaryMessenger = binaryMessenger; + } + return self; +} +- (void)didReceiveScriptMessageForHandlerWithIdentifier:(NSNumber *)arg_identifier + userContentControllerIdentifier: + (NSNumber *)arg_userContentControllerIdentifier + message:(FWFWKScriptMessageData *)arg_message + completion:(void (^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName: + @"dev.flutter.pigeon.WKScriptMessageHandlerFlutterApi.didReceiveScriptMessage" + binaryMessenger:self.binaryMessenger + codec:FWFWKScriptMessageHandlerFlutterApiGetCodec()]; + [channel sendMessage:@[ + arg_identifier ?: [NSNull null], arg_userContentControllerIdentifier ?: [NSNull null], + arg_message ?: [NSNull null] + ] + reply:^(id reply) { + completion(nil); + }]; +} +@end +@interface FWFWKNavigationDelegateHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKNavigationDelegateHostApiCodecReader +@end + +@interface FWFWKNavigationDelegateHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKNavigationDelegateHostApiCodecWriter +@end + +@interface FWFWKNavigationDelegateHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKNavigationDelegateHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKNavigationDelegateHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKNavigationDelegateHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKNavigationDelegateHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKNavigationDelegateHostApiCodecReaderWriter *readerWriter = + [[FWFWKNavigationDelegateHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKNavigationDelegateHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKNavigationDelegateHostApi.create" + binaryMessenger:binaryMessenger + codec:FWFWKNavigationDelegateHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createWithIdentifier:error:)], + @"FWFWKNavigationDelegateHostApi api (%@) doesn't respond to " + @"@selector(createWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api createWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKNavigationDelegateFlutterApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKNavigationDelegateFlutterApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFNSErrorData fromMap:[self readValue]]; + + case 129: + return [FWFNSUrlRequestData fromMap:[self readValue]]; + + case 130: + return [FWFWKFrameInfoData fromMap:[self readValue]]; + + case 131: + return [FWFWKNavigationActionData fromMap:[self readValue]]; + + case 132: + return [FWFWKNavigationActionPolicyEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFWKNavigationDelegateFlutterApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKNavigationDelegateFlutterApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFNSErrorData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSUrlRequestData class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { + [self writeByte:130]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { + [self writeByte:131]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { + [self writeByte:132]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFWKNavigationDelegateFlutterApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKNavigationDelegateFlutterApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKNavigationDelegateFlutterApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKNavigationDelegateFlutterApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKNavigationDelegateFlutterApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKNavigationDelegateFlutterApiCodecReaderWriter *readerWriter = + [[FWFWKNavigationDelegateFlutterApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +@interface FWFWKNavigationDelegateFlutterApi () +@property(nonatomic, strong) NSObject *binaryMessenger; +@end + +@implementation FWFWKNavigationDelegateFlutterApi + +- (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { + self = [super init]; + if (self) { + _binaryMessenger = binaryMessenger; + } + return self; +} +- (void)didFinishNavigationForDelegateWithIdentifier:(NSNumber *)arg_identifier + webViewIdentifier:(NSNumber *)arg_webViewIdentifier + URL:(nullable NSString *)arg_url + completion:(void (^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName: + @"dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFinishNavigation" + binaryMessenger:self.binaryMessenger + codec:FWFWKNavigationDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[ + arg_identifier ?: [NSNull null], arg_webViewIdentifier ?: [NSNull null], + arg_url ?: [NSNull null] + ] + reply:^(id reply) { + completion(nil); + }]; +} +- (void)didStartProvisionalNavigationForDelegateWithIdentifier:(NSNumber *)arg_identifier + webViewIdentifier:(NSNumber *)arg_webViewIdentifier + URL:(nullable NSString *)arg_url + completion: + (void (^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName: + @"dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didStartProvisionalNavigation" + binaryMessenger:self.binaryMessenger + codec:FWFWKNavigationDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[ + arg_identifier ?: [NSNull null], arg_webViewIdentifier ?: [NSNull null], + arg_url ?: [NSNull null] + ] + reply:^(id reply) { + completion(nil); + }]; +} +- (void) + decidePolicyForNavigationActionForDelegateWithIdentifier:(NSNumber *)arg_identifier + webViewIdentifier:(NSNumber *)arg_webViewIdentifier + navigationAction: + (FWFWKNavigationActionData *)arg_navigationAction + completion: + (void (^)(FWFWKNavigationActionPolicyEnumData + *_Nullable, + NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName: + @"dev.flutter.pigeon.WKNavigationDelegateFlutterApi.decidePolicyForNavigationAction" + binaryMessenger:self.binaryMessenger + codec:FWFWKNavigationDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[ + arg_identifier ?: [NSNull null], arg_webViewIdentifier ?: [NSNull null], + arg_navigationAction ?: [NSNull null] + ] + reply:^(id reply) { + FWFWKNavigationActionPolicyEnumData *output = reply; + completion(output, nil); + }]; +} +- (void)didFailNavigationForDelegateWithIdentifier:(NSNumber *)arg_identifier + webViewIdentifier:(NSNumber *)arg_webViewIdentifier + error:(FWFNSErrorData *)arg_error + completion:(void (^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFailNavigation" + binaryMessenger:self.binaryMessenger + codec:FWFWKNavigationDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[ + arg_identifier ?: [NSNull null], arg_webViewIdentifier ?: [NSNull null], + arg_error ?: [NSNull null] + ] + reply:^(id reply) { + completion(nil); + }]; +} +- (void)didFailProvisionalNavigationForDelegateWithIdentifier:(NSNumber *)arg_identifier + webViewIdentifier:(NSNumber *)arg_webViewIdentifier + error:(FWFNSErrorData *)arg_error + completion: + (void (^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName: + @"dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFailProvisionalNavigation" + binaryMessenger:self.binaryMessenger + codec:FWFWKNavigationDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[ + arg_identifier ?: [NSNull null], arg_webViewIdentifier ?: [NSNull null], + arg_error ?: [NSNull null] + ] + reply:^(id reply) { + completion(nil); + }]; +} +- (void)webViewWebContentProcessDidTerminateForDelegateWithIdentifier:(NSNumber *)arg_identifier + webViewIdentifier: + (NSNumber *)arg_webViewIdentifier + completion:(void (^)(NSError *_Nullable)) + completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName: + @"dev.flutter.pigeon.WKNavigationDelegateFlutterApi.webViewWebContentProcessDidTerminate" + binaryMessenger:self.binaryMessenger + codec:FWFWKNavigationDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[ arg_identifier ?: [NSNull null], arg_webViewIdentifier ?: [NSNull null] ] + reply:^(id reply) { + completion(nil); + }]; +} +@end +@interface FWFNSObjectHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFNSObjectHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFNSKeyValueObservingOptionsEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFNSObjectHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFNSObjectHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFNSKeyValueObservingOptionsEnumData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFNSObjectHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFNSObjectHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFNSObjectHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFNSObjectHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFNSObjectHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFNSObjectHostApiCodecReaderWriter *readerWriter = + [[FWFNSObjectHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFNSObjectHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NSObjectHostApi.dispose" + binaryMessenger:binaryMessenger + codec:FWFNSObjectHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(disposeObjectWithIdentifier:error:)], + @"FWFNSObjectHostApi api (%@) doesn't respond to " + @"@selector(disposeObjectWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api disposeObjectWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NSObjectHostApi.addObserver" + binaryMessenger:binaryMessenger + codec:FWFNSObjectHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (addObserverForObjectWithIdentifier: + observerIdentifier:keyPath:options:error:)], + @"FWFNSObjectHostApi api (%@) doesn't respond to " + @"@selector(addObserverForObjectWithIdentifier:observerIdentifier:keyPath:options:" + @"error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_observerIdentifier = GetNullableObjectAtIndex(args, 1); + NSString *arg_keyPath = GetNullableObjectAtIndex(args, 2); + NSArray *arg_options = + GetNullableObjectAtIndex(args, 3); + FlutterError *error; + [api addObserverForObjectWithIdentifier:arg_identifier + observerIdentifier:arg_observerIdentifier + keyPath:arg_keyPath + options:arg_options + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NSObjectHostApi.removeObserver" + binaryMessenger:binaryMessenger + codec:FWFNSObjectHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(removeObserverForObjectWithIdentifier: + observerIdentifier:keyPath:error:)], + @"FWFNSObjectHostApi api (%@) doesn't respond to " + @"@selector(removeObserverForObjectWithIdentifier:observerIdentifier:keyPath:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_observerIdentifier = GetNullableObjectAtIndex(args, 1); + NSString *arg_keyPath = GetNullableObjectAtIndex(args, 2); + FlutterError *error; + [api removeObserverForObjectWithIdentifier:arg_identifier + observerIdentifier:arg_observerIdentifier + keyPath:arg_keyPath + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFNSObjectFlutterApiCodecReader : FlutterStandardReader +@end +@implementation FWFNSObjectFlutterApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFNSErrorData fromMap:[self readValue]]; + + case 129: + return [FWFNSHttpCookieData fromMap:[self readValue]]; + + case 130: + return [FWFNSHttpCookiePropertyKeyEnumData fromMap:[self readValue]]; + + case 131: + return [FWFNSKeyValueChangeKeyEnumData fromMap:[self readValue]]; + + case 132: + return [FWFNSKeyValueObservingOptionsEnumData fromMap:[self readValue]]; + + case 133: + return [FWFNSUrlRequestData fromMap:[self readValue]]; + + case 134: + return [FWFWKAudiovisualMediaTypeEnumData fromMap:[self readValue]]; + + case 135: + return [FWFWKFrameInfoData fromMap:[self readValue]]; + + case 136: + return [FWFWKNavigationActionData fromMap:[self readValue]]; + + case 137: + return [FWFWKNavigationActionPolicyEnumData fromMap:[self readValue]]; + + case 138: + return [FWFWKScriptMessageData fromMap:[self readValue]]; + + case 139: + return [FWFWKUserScriptData fromMap:[self readValue]]; + + case 140: + return [FWFWKUserScriptInjectionTimeEnumData fromMap:[self readValue]]; + + case 141: + return [FWFWKWebsiteDataTypeEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFNSObjectFlutterApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFNSObjectFlutterApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFNSErrorData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSHttpCookieData class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSHttpCookiePropertyKeyEnumData class]]) { + [self writeByte:130]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSKeyValueChangeKeyEnumData class]]) { + [self writeByte:131]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSKeyValueObservingOptionsEnumData class]]) { + [self writeByte:132]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSUrlRequestData class]]) { + [self writeByte:133]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKAudiovisualMediaTypeEnumData class]]) { + [self writeByte:134]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { + [self writeByte:135]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { + [self writeByte:136]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { + [self writeByte:137]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKScriptMessageData class]]) { + [self writeByte:138]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKUserScriptData class]]) { + [self writeByte:139]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { + [self writeByte:140]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKWebsiteDataTypeEnumData class]]) { + [self writeByte:141]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFNSObjectFlutterApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFNSObjectFlutterApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFNSObjectFlutterApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFNSObjectFlutterApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFNSObjectFlutterApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFNSObjectFlutterApiCodecReaderWriter *readerWriter = + [[FWFNSObjectFlutterApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +@interface FWFNSObjectFlutterApi () +@property(nonatomic, strong) NSObject *binaryMessenger; +@end + +@implementation FWFNSObjectFlutterApi + +- (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { + self = [super init]; + if (self) { + _binaryMessenger = binaryMessenger; + } + return self; +} +- (void)observeValueForObjectWithIdentifier:(NSNumber *)arg_identifier + keyPath:(NSString *)arg_keyPath + objectIdentifier:(NSNumber *)arg_objectIdentifier + changeKeys: + (NSArray *)arg_changeKeys + changeValues:(NSArray *)arg_changeValues + completion:(void (^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.NSObjectFlutterApi.observeValue" + binaryMessenger:self.binaryMessenger + codec:FWFNSObjectFlutterApiGetCodec()]; + [channel sendMessage:@[ + arg_identifier ?: [NSNull null], arg_keyPath ?: [NSNull null], + arg_objectIdentifier ?: [NSNull null], arg_changeKeys ?: [NSNull null], + arg_changeValues ?: [NSNull null] + ] + reply:^(id reply) { + completion(nil); + }]; +} +- (void)disposeObjectWithIdentifier:(NSNumber *)arg_identifier + completion:(void (^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.NSObjectFlutterApi.dispose" + binaryMessenger:self.binaryMessenger + codec:FWFNSObjectFlutterApiGetCodec()]; + [channel sendMessage:@[ arg_identifier ?: [NSNull null] ] + reply:^(id reply) { + completion(nil); + }]; +} +@end +@interface FWFWKWebViewHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKWebViewHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFNSErrorData fromMap:[self readValue]]; + + case 129: + return [FWFNSHttpCookieData fromMap:[self readValue]]; + + case 130: + return [FWFNSHttpCookiePropertyKeyEnumData fromMap:[self readValue]]; + + case 131: + return [FWFNSKeyValueChangeKeyEnumData fromMap:[self readValue]]; + + case 132: + return [FWFNSKeyValueObservingOptionsEnumData fromMap:[self readValue]]; + + case 133: + return [FWFNSUrlRequestData fromMap:[self readValue]]; + + case 134: + return [FWFWKAudiovisualMediaTypeEnumData fromMap:[self readValue]]; + + case 135: + return [FWFWKFrameInfoData fromMap:[self readValue]]; + + case 136: + return [FWFWKNavigationActionData fromMap:[self readValue]]; + + case 137: + return [FWFWKNavigationActionPolicyEnumData fromMap:[self readValue]]; + + case 138: + return [FWFWKScriptMessageData fromMap:[self readValue]]; + + case 139: + return [FWFWKUserScriptData fromMap:[self readValue]]; + + case 140: + return [FWFWKUserScriptInjectionTimeEnumData fromMap:[self readValue]]; + + case 141: + return [FWFWKWebsiteDataTypeEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFWKWebViewHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKWebViewHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFNSErrorData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSHttpCookieData class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSHttpCookiePropertyKeyEnumData class]]) { + [self writeByte:130]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSKeyValueChangeKeyEnumData class]]) { + [self writeByte:131]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSKeyValueObservingOptionsEnumData class]]) { + [self writeByte:132]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSUrlRequestData class]]) { + [self writeByte:133]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKAudiovisualMediaTypeEnumData class]]) { + [self writeByte:134]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { + [self writeByte:135]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { + [self writeByte:136]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { + [self writeByte:137]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKScriptMessageData class]]) { + [self writeByte:138]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKUserScriptData class]]) { + [self writeByte:139]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { + [self writeByte:140]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKWebsiteDataTypeEnumData class]]) { + [self writeByte:141]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFWKWebViewHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKWebViewHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKWebViewHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKWebViewHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKWebViewHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKWebViewHostApiCodecReaderWriter *readerWriter = + [[FWFWKWebViewHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKWebViewHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.create" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createWithIdentifier: + configurationIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(createWithIdentifier:configurationIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_configurationIdentifier = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createWithIdentifier:arg_identifier + configurationIdentifier:arg_configurationIdentifier + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.setUIDelegate" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setUIDelegateForWebViewWithIdentifier: + delegateIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(setUIDelegateForWebViewWithIdentifier:delegateIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_uiDelegateIdentifier = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setUIDelegateForWebViewWithIdentifier:arg_identifier + delegateIdentifier:arg_uiDelegateIdentifier + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.setNavigationDelegate" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(setNavigationDelegateForWebViewWithIdentifier: + delegateIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(setNavigationDelegateForWebViewWithIdentifier:delegateIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_navigationDelegateIdentifier = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setNavigationDelegateForWebViewWithIdentifier:arg_identifier + delegateIdentifier:arg_navigationDelegateIdentifier + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.getUrl" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(URLForWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(URLForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + NSString *output = [api URLForWebViewWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.getEstimatedProgress" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(estimatedProgressForWebViewWithIdentifier: + error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(estimatedProgressForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + NSNumber *output = [api estimatedProgressForWebViewWithIdentifier:arg_identifier + error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.loadRequest" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(loadRequestForWebViewWithIdentifier: + request:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(loadRequestForWebViewWithIdentifier:request:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FWFNSUrlRequestData *arg_request = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api loadRequestForWebViewWithIdentifier:arg_identifier request:arg_request error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.loadHtmlString" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(loadHTMLForWebViewWithIdentifier: + HTMLString:baseURL:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(loadHTMLForWebViewWithIdentifier:HTMLString:baseURL:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSString *arg_string = GetNullableObjectAtIndex(args, 1); + NSString *arg_baseUrl = GetNullableObjectAtIndex(args, 2); + FlutterError *error; + [api loadHTMLForWebViewWithIdentifier:arg_identifier + HTMLString:arg_string + baseURL:arg_baseUrl + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (loadFileForWebViewWithIdentifier:fileURL:readAccessURL:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(loadFileForWebViewWithIdentifier:fileURL:readAccessURL:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSString *arg_url = GetNullableObjectAtIndex(args, 1); + NSString *arg_readAccessUrl = GetNullableObjectAtIndex(args, 2); + FlutterError *error; + [api loadFileForWebViewWithIdentifier:arg_identifier + fileURL:arg_url + readAccessURL:arg_readAccessUrl + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.loadFlutterAsset" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(loadAssetForWebViewWithIdentifier: + assetKey:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(loadAssetForWebViewWithIdentifier:assetKey:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSString *arg_key = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api loadAssetForWebViewWithIdentifier:arg_identifier assetKey:arg_key error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.canGoBack" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(canGoBackForWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(canGoBackForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + NSNumber *output = [api canGoBackForWebViewWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.canGoForward" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(canGoForwardForWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(canGoForwardForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + NSNumber *output = [api canGoForwardForWebViewWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.goBack" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(goBackForWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(goBackForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api goBackForWebViewWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.goForward" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(goForwardForWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(goForwardForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api goForwardForWebViewWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.reload" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(reloadWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(reloadWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api reloadWebViewWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.getTitle" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(titleForWebViewWithIdentifier:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(titleForWebViewWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + NSString *output = [api titleForWebViewWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.WKWebViewHostApi.setAllowsBackForwardNavigationGestures" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (setAllowsBackForwardForWebViewWithIdentifier:isAllowed:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(setAllowsBackForwardForWebViewWithIdentifier:isAllowed:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_allow = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setAllowsBackForwardForWebViewWithIdentifier:arg_identifier + isAllowed:arg_allow + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.setCustomUserAgent" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setUserAgentForWebViewWithIdentifier: + userAgent:error:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(setUserAgentForWebViewWithIdentifier:userAgent:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSString *arg_userAgent = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setUserAgentForWebViewWithIdentifier:arg_identifier + userAgent:arg_userAgent + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.evaluateJavaScript" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewHostApiGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector + (evaluateJavaScriptForWebViewWithIdentifier:javaScriptString:completion:)], + @"FWFWKWebViewHostApi api (%@) doesn't respond to " + @"@selector(evaluateJavaScriptForWebViewWithIdentifier:javaScriptString:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSString *arg_javaScriptString = GetNullableObjectAtIndex(args, 1); + [api evaluateJavaScriptForWebViewWithIdentifier:arg_identifier + javaScriptString:arg_javaScriptString + completion:^(id _Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKUIDelegateHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKUIDelegateHostApiCodecReader +@end + +@interface FWFWKUIDelegateHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKUIDelegateHostApiCodecWriter +@end + +@interface FWFWKUIDelegateHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKUIDelegateHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKUIDelegateHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKUIDelegateHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKUIDelegateHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKUIDelegateHostApiCodecReaderWriter *readerWriter = + [[FWFWKUIDelegateHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKUIDelegateHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKUIDelegateHostApi.create" + binaryMessenger:binaryMessenger + codec:FWFWKUIDelegateHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createWithIdentifier:error:)], + @"FWFWKUIDelegateHostApi api (%@) doesn't respond to " + @"@selector(createWithIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api createWithIdentifier:arg_identifier error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +@interface FWFWKUIDelegateFlutterApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKUIDelegateFlutterApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFNSUrlRequestData fromMap:[self readValue]]; + + case 129: + return [FWFWKFrameInfoData fromMap:[self readValue]]; + + case 130: + return [FWFWKNavigationActionData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFWKUIDelegateFlutterApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKUIDelegateFlutterApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFNSUrlRequestData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { + [self writeByte:130]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFWKUIDelegateFlutterApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKUIDelegateFlutterApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKUIDelegateFlutterApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKUIDelegateFlutterApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKUIDelegateFlutterApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKUIDelegateFlutterApiCodecReaderWriter *readerWriter = + [[FWFWKUIDelegateFlutterApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +@interface FWFWKUIDelegateFlutterApi () +@property(nonatomic, strong) NSObject *binaryMessenger; +@end + +@implementation FWFWKUIDelegateFlutterApi + +- (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { + self = [super init]; + if (self) { + _binaryMessenger = binaryMessenger; + } + return self; +} +- (void)onCreateWebViewForDelegateWithIdentifier:(NSNumber *)arg_identifier + webViewIdentifier:(NSNumber *)arg_webViewIdentifier + configurationIdentifier:(NSNumber *)arg_configurationIdentifier + navigationAction:(FWFWKNavigationActionData *)arg_navigationAction + completion:(void (^)(NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.WKUIDelegateFlutterApi.onCreateWebView" + binaryMessenger:self.binaryMessenger + codec:FWFWKUIDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[ + arg_identifier ?: [NSNull null], arg_webViewIdentifier ?: [NSNull null], + arg_configurationIdentifier ?: [NSNull null], arg_navigationAction ?: [NSNull null] + ] + reply:^(id reply) { + completion(nil); + }]; +} +@end +@interface FWFWKHttpCookieStoreHostApiCodecReader : FlutterStandardReader +@end +@implementation FWFWKHttpCookieStoreHostApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FWFNSHttpCookieData fromMap:[self readValue]]; + + case 129: + return [FWFNSHttpCookiePropertyKeyEnumData fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FWFWKHttpCookieStoreHostApiCodecWriter : FlutterStandardWriter +@end +@implementation FWFWKHttpCookieStoreHostApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FWFNSHttpCookieData class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FWFNSHttpCookiePropertyKeyEnumData class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FWFWKHttpCookieStoreHostApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FWFWKHttpCookieStoreHostApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FWFWKHttpCookieStoreHostApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FWFWKHttpCookieStoreHostApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FWFWKHttpCookieStoreHostApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FWFWKHttpCookieStoreHostApiCodecReaderWriter *readerWriter = + [[FWFWKHttpCookieStoreHostApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FWFWKHttpCookieStoreHostApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKHttpCookieStoreHostApi.createFromWebsiteDataStore" + binaryMessenger:binaryMessenger + codec:FWFWKHttpCookieStoreHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(createFromWebsiteDataStoreWithIdentifier: + dataStoreIdentifier:error:)], + @"FWFWKHttpCookieStoreHostApi api (%@) doesn't respond to " + @"@selector(createFromWebsiteDataStoreWithIdentifier:dataStoreIdentifier:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_websiteDataStoreIdentifier = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api createFromWebsiteDataStoreWithIdentifier:arg_identifier + dataStoreIdentifier:arg_websiteDataStoreIdentifier + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKHttpCookieStoreHostApi.setCookie" + binaryMessenger:binaryMessenger + codec:FWFWKHttpCookieStoreHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setCookieForStoreWithIdentifier: + cookie:completion:)], + @"FWFWKHttpCookieStoreHostApi api (%@) doesn't respond to " + @"@selector(setCookieForStoreWithIdentifier:cookie:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + FWFNSHttpCookieData *arg_cookie = GetNullableObjectAtIndex(args, 1); + [api setCookieForStoreWithIdentifier:arg_identifier + cookie:arg_cookie + completion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.h new file mode 100644 index 000000000000..887c9f1b3d8b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#import "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Host api implementation for WKHTTPCookieStore. + * + * Handles creating WKHTTPCookieStore that intercommunicate with a paired Dart object. + */ +@interface FWFHTTPCookieStoreHostApiImpl : NSObject +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.m new file mode 100644 index 000000000000..79a3a684b805 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.m @@ -0,0 +1,61 @@ +// 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 "FWFHTTPCookieStoreHostApi.h" +#import "FWFDataConverters.h" +#import "FWFWebsiteDataStoreHostApi.h" + +@interface FWFHTTPCookieStoreHostApiImpl () +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFHTTPCookieStoreHostApiImpl +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _instanceManager = instanceManager; + } + return self; +} + +- (WKHTTPCookieStore *)HTTPCookieStoreForIdentifier:(NSNumber *)identifier + API_AVAILABLE(ios(11.0)) { + return (WKHTTPCookieStore *)[self.instanceManager instanceForIdentifier:identifier.longValue]; +} + +- (void)createFromWebsiteDataStoreWithIdentifier:(nonnull NSNumber *)identifier + dataStoreIdentifier:(nonnull NSNumber *)websiteDataStoreIdentifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull) + error { + if (@available(iOS 11.0, *)) { + WKWebsiteDataStore *dataStore = (WKWebsiteDataStore *)[self.instanceManager + instanceForIdentifier:websiteDataStoreIdentifier.longValue]; + [self.instanceManager addDartCreatedInstance:dataStore.httpCookieStore + withIdentifier:identifier.longValue]; + } else { + *error = [FlutterError + errorWithCode:@"FWFUnsupportedVersionError" + message:@"WKWebsiteDataStore.httpCookieStore is only supported on versions 11+." + details:nil]; + } +} + +- (void)setCookieForStoreWithIdentifier:(nonnull NSNumber *)identifier + cookie:(nonnull FWFNSHttpCookieData *)cookie + completion:(nonnull void (^)(FlutterError *_Nullable))completion { + NSHTTPCookie *nsCookie = FWFNSHTTPCookieFromCookieData(cookie); + + if (@available(iOS 11.0, *)) { + [[self HTTPCookieStoreForIdentifier:identifier] setCookie:nsCookie + completionHandler:^{ + completion(nil); + }]; + } else { + completion([FlutterError errorWithCode:@"FWFUnsupportedVersionError" + message:@"setCookie is only supported on versions 11+." + details:nil]); + } +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager.h new file mode 100644 index 000000000000..5dec08055ce5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager.h @@ -0,0 +1,97 @@ +// 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 + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^FWFOnDeallocCallback)(long identifier); + +/** + * Maintains instances used to communicate with the corresponding objects in Dart. + * + * When an instance is added with an identifier, either can be used to retrieve the other. + * + * Added instances are added as a weak reference and a strong reference. When the strong reference + * is removed with `removeStrongReferenceWithIdentifier:` and the weak reference is deallocated, + * the `deallocCallback` is made with the instance's identifier. However, if the strong reference is + * removed and then the identifier is retrieved with the intention to pass the identifier to Dart + * (e.g. calling `identifierForInstance:identifierWillBePassedToFlutter:` with + * `identifierWillBePassedToFlutter` set to YES), the strong reference to the instance is recreated. + * The strong reference will then need to be removed manually again. + * + * Accessing and inserting to an InstanceManager is thread safe. + */ +@interface FWFInstanceManager : NSObject +@property(readonly) FWFOnDeallocCallback deallocCallback; +- (instancetype)initWithDeallocCallback:(FWFOnDeallocCallback)callback; +// TODO(bparrishMines): Pairs should not be able to be overwritten and this feature +// should be replaced with a call to clear the manager in the event of a hot restart. +/** + * Adds a new instance that was instantiated from Dart. + * + * If an instance or identifier has already been added, it will be replaced by the new values. The + * Dart InstanceManager is considered the source of truth and has the capability to overwrite stored + * pairs in response to hot restarts. + * + * @param instance The instance to be stored. + * @param instanceIdentifier The identifier to be paired with instance. This value must be >= 0. + */ +- (void)addDartCreatedInstance:(NSObject *)instance withIdentifier:(long)instanceIdentifier; + +/** + * Adds a new instance that was instantiated from the host platform. + * + * @param instance The instance to be stored. + * @return The unique identifier stored with instance. + */ +- (long)addHostCreatedInstance:(nonnull NSObject *)instance; + +/** + * Removes `instanceIdentifier` and its associated strongly referenced instance, if present, from + * the manager. + * + * @param instanceIdentifier The identifier paired to an instance. + * + * @return The removed instance if the manager contains the given instanceIdentifier, otherwise + * nil. + */ +- (nullable NSObject *)removeInstanceWithIdentifier:(long)instanceIdentifier; + +/** + * Retrieves the instance associated with identifier. + * + * @param instanceIdentifier The identifier paired to an instance. + * + * @return The instance associated with `instanceIdentifier` if the manager contains the value, + * otherwise nil. + */ +- (nullable NSObject *)instanceForIdentifier:(long)instanceIdentifier; + +/** + * Retrieves the identifier paired with an instance. + * + * If the manager contains `instance`, as a strong or weak reference, the strong reference to + * `instance` will be recreated and will need to be removed again with + * `removeInstanceWithIdentifier:`. + * + * This method also expects the Dart `InstanceManager` to have, or recreate, a weak reference to the + * instance the identifier is associated with once it receives it. + * + * @param instance An instance that may be stored in the manager. + * + * @return The identifier associated with `instance` if the manager contains the value, otherwise + * NSNotFound. + */ +- (long)identifierWithStrongReferenceForInstance:(nonnull NSObject *)instance; + +/** + * Returns whether this manager contains the given `instance`. + * + * @return Whether this manager contains the given `instance`. + */ +- (BOOL)containsInstance:(nonnull NSObject *)instance; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager.m new file mode 100644 index 000000000000..e87a4037bd04 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager.m @@ -0,0 +1,166 @@ +// 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 "FWFInstanceManager.h" +#import "FWFInstanceManager_Test.h" + +#import + +// Attaches to an object to receive a callback when the object is deallocated. +@interface FWFFinalizer : NSObject +@end + +// Attaches to an object to receive a callback when the object is deallocated. +@implementation FWFFinalizer { + long _identifier; + // Callbacks are no longer made once FWFInstanceManager is inaccessible. + FWFOnDeallocCallback __weak _callback; +} + +- (instancetype)initWithIdentifier:(long)identifier callback:(FWFOnDeallocCallback)callback { + self = [self init]; + if (self) { + _identifier = identifier; + _callback = callback; + } + return self; +} + ++ (void)attachToInstance:(NSObject *)instance + withIdentifier:(long)identifier + callback:(FWFOnDeallocCallback)callback { + FWFFinalizer *finalizer = [[FWFFinalizer alloc] initWithIdentifier:identifier callback:callback]; + objc_setAssociatedObject(instance, _cmd, finalizer, OBJC_ASSOCIATION_RETAIN); +} + ++ (void)detachFromInstance:(NSObject *)instance { + objc_setAssociatedObject(instance, @selector(attachToInstance:withIdentifier:callback:), nil, + OBJC_ASSOCIATION_ASSIGN); +} + +- (void)dealloc { + if (_callback) { + _callback(_identifier); + } +} +@end + +@interface FWFInstanceManager () +@property dispatch_queue_t lockQueue; +@property NSMapTable *identifiers; +@property NSMapTable *weakInstances; +@property NSMapTable *strongInstances; +@property long nextIdentifier; +@end + +@implementation FWFInstanceManager +// Identifiers are locked to a specific range to avoid collisions with objects +// created simultaneously from Dart. +// Host uses identifiers >= 2^16 and Dart is expected to use values n where, +// 0 <= n < 2^16. +static long const FWFMinHostCreatedIdentifier = 65536; + +- (instancetype)init { + self = [super init]; + if (self) { + _deallocCallback = _deallocCallback ? _deallocCallback : ^(long identifier) { + }; + _lockQueue = dispatch_queue_create("FWFInstanceManager", DISPATCH_QUEUE_SERIAL); + _identifiers = [NSMapTable weakToStrongObjectsMapTable]; + _weakInstances = [NSMapTable strongToWeakObjectsMapTable]; + _strongInstances = [NSMapTable strongToStrongObjectsMapTable]; + _nextIdentifier = FWFMinHostCreatedIdentifier; + } + return self; +} + +- (instancetype)initWithDeallocCallback:(FWFOnDeallocCallback)callback { + self = [self init]; + if (self) { + _deallocCallback = callback; + } + return self; +} + +- (void)addDartCreatedInstance:(NSObject *)instance withIdentifier:(long)instanceIdentifier { + NSParameterAssert(instance); + NSParameterAssert(instanceIdentifier >= 0); + dispatch_async(_lockQueue, ^{ + [self addInstance:instance withIdentifier:instanceIdentifier]; + }); +} + +- (long)addHostCreatedInstance:(nonnull NSObject *)instance { + NSParameterAssert(instance); + long __block identifier = -1; + dispatch_sync(_lockQueue, ^{ + identifier = self.nextIdentifier++; + [self addInstance:instance withIdentifier:identifier]; + }); + return identifier; +} + +- (nullable NSObject *)removeInstanceWithIdentifier:(long)instanceIdentifier { + NSObject *__block instance = nil; + dispatch_sync(_lockQueue, ^{ + instance = [self.strongInstances objectForKey:@(instanceIdentifier)]; + if (instance) { + [self.strongInstances removeObjectForKey:@(instanceIdentifier)]; + } + }); + return instance; +} + +- (nullable NSObject *)instanceForIdentifier:(long)instanceIdentifier { + NSObject *__block instance = nil; + dispatch_sync(_lockQueue, ^{ + instance = [self.weakInstances objectForKey:@(instanceIdentifier)]; + }); + return instance; +} + +- (void)addInstance:(nonnull NSObject *)instance withIdentifier:(long)instanceIdentifier { + [self.identifiers setObject:@(instanceIdentifier) forKey:instance]; + [self.weakInstances setObject:instance forKey:@(instanceIdentifier)]; + [self.strongInstances setObject:instance forKey:@(instanceIdentifier)]; + [FWFFinalizer attachToInstance:instance + withIdentifier:instanceIdentifier + callback:self.deallocCallback]; +} + +- (long)identifierWithStrongReferenceForInstance:(nonnull NSObject *)instance { + NSNumber *__block identifierNumber = nil; + dispatch_sync(_lockQueue, ^{ + identifierNumber = [self.identifiers objectForKey:instance]; + if (identifierNumber) { + [self.strongInstances setObject:instance forKey:identifierNumber]; + } + }); + return identifierNumber ? identifierNumber.longValue : NSNotFound; +} + +- (BOOL)containsInstance:(nonnull NSObject *)instance { + BOOL __block containsInstance; + dispatch_sync(_lockQueue, ^{ + containsInstance = [self.identifiers objectForKey:instance]; + }); + return containsInstance; +} + +- (NSUInteger)strongInstanceCount { + NSUInteger __block count = -1; + dispatch_sync(_lockQueue, ^{ + count = self.strongInstances.count; + }); + return count; +} + +- (NSUInteger)weakInstanceCount { + NSUInteger __block count = -1; + dispatch_sync(_lockQueue, ^{ + count = self.weakInstances.count; + }); + return count; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager_Test.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager_Test.h new file mode 100644 index 000000000000..4f609049de0e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager_Test.h @@ -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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FWFInstanceManager () +/** + * The number of instances stored as a strong reference. + * + * Added for debugging purposes. + */ +- (NSUInteger)strongInstanceCount; + +/** + * The number of instances stored as a weak reference. + * + * Added for debugging purposes. NSMapTables that store keys or objects as weak reference will be + * reclaimed nondeterministically. + */ +- (NSUInteger)weakInstanceCount; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.h new file mode 100644 index 000000000000..90e55417cd1b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.h @@ -0,0 +1,45 @@ +// 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 + +#import "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" +#import "FWFObjectHostApi.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Flutter api implementation for WKNavigationDelegate. + * + * Handles making callbacks to Dart for a WKNavigationDelegate. + */ +@interface FWFNavigationDelegateFlutterApiImpl : FWFWKNavigationDelegateFlutterApi +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +/** + * Implementation of WKNavigationDelegate for FWFNavigationDelegateHostApiImpl. + */ +@interface FWFNavigationDelegate : FWFObject +@property(readonly, nonnull, nonatomic) FWFNavigationDelegateFlutterApiImpl *navigationDelegateAPI; + +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +/** + * Host api implementation for WKNavigationDelegate. + * + * Handles creating WKNavigationDelegate that intercommunicate with a paired Dart object. + */ +@interface FWFNavigationDelegateHostApiImpl : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m new file mode 100644 index 000000000000..1132e02880b2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m @@ -0,0 +1,216 @@ +// 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 "FWFNavigationDelegateHostApi.h" +#import "FWFDataConverters.h" +#import "FWFWebViewConfigurationHostApi.h" + +@interface FWFNavigationDelegateFlutterApiImpl () +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFNavigationDelegateFlutterApiImpl +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self initWithBinaryMessenger:binaryMessenger]; + if (self) { + _instanceManager = instanceManager; + } + return self; +} + +- (long)identifierForDelegate:(FWFNavigationDelegate *)instance { + return [self.instanceManager identifierWithStrongReferenceForInstance:instance]; +} + +- (void)didFinishNavigationForDelegate:(FWFNavigationDelegate *)instance + webView:(WKWebView *)webView + URL:(NSString *)URL + completion:(void (^)(NSError *_Nullable))completion { + NSNumber *webViewIdentifier = + @([self.instanceManager identifierWithStrongReferenceForInstance:webView]); + [self didFinishNavigationForDelegateWithIdentifier:@([self identifierForDelegate:instance]) + webViewIdentifier:webViewIdentifier + URL:URL + completion:completion]; +} + +- (void)didStartProvisionalNavigationForDelegate:(FWFNavigationDelegate *)instance + webView:(WKWebView *)webView + URL:(NSString *)URL + completion:(void (^)(NSError *_Nullable))completion { + NSNumber *webViewIdentifier = + @([self.instanceManager identifierWithStrongReferenceForInstance:webView]); + [self didStartProvisionalNavigationForDelegateWithIdentifier:@([self + identifierForDelegate:instance]) + webViewIdentifier:webViewIdentifier + URL:URL + completion:completion]; +} + +- (void) + decidePolicyForNavigationActionForDelegate:(FWFNavigationDelegate *)instance + webView:(WKWebView *)webView + navigationAction:(WKNavigationAction *)navigationAction + completion: + (void (^)(FWFWKNavigationActionPolicyEnumData *_Nullable, + NSError *_Nullable))completion { + NSNumber *webViewIdentifier = + @([self.instanceManager identifierWithStrongReferenceForInstance:webView]); + FWFWKNavigationActionData *navigationActionData = + FWFWKNavigationActionDataFromNavigationAction(navigationAction); + [self + decidePolicyForNavigationActionForDelegateWithIdentifier:@([self + identifierForDelegate:instance]) + webViewIdentifier:webViewIdentifier + navigationAction:navigationActionData + completion:completion]; +} + +- (void)didFailNavigationForDelegate:(FWFNavigationDelegate *)instance + webView:(WKWebView *)webView + error:(NSError *)error + completion:(void (^)(NSError *_Nullable))completion { + NSNumber *webViewIdentifier = + @([self.instanceManager identifierWithStrongReferenceForInstance:webView]); + [self didFailNavigationForDelegateWithIdentifier:@([self identifierForDelegate:instance]) + webViewIdentifier:webViewIdentifier + error:FWFNSErrorDataFromNSError(error) + completion:completion]; +} + +- (void)didFailProvisionalNavigationForDelegate:(FWFNavigationDelegate *)instance + webView:(WKWebView *)webView + error:(NSError *)error + completion:(void (^)(NSError *_Nullable))completion { + NSNumber *webViewIdentifier = + @([self.instanceManager identifierWithStrongReferenceForInstance:webView]); + [self + didFailProvisionalNavigationForDelegateWithIdentifier:@([self identifierForDelegate:instance]) + webViewIdentifier:webViewIdentifier + error:FWFNSErrorDataFromNSError(error) + completion:completion]; +} + +- (void)webViewWebContentProcessDidTerminateForDelegate:(FWFNavigationDelegate *)instance + webView:(WKWebView *)webView + completion:(void (^)(NSError *_Nullable))completion { + NSNumber *webViewIdentifier = + @([self.instanceManager identifierWithStrongReferenceForInstance:webView]); + [self webViewWebContentProcessDidTerminateForDelegateWithIdentifier: + @([self identifierForDelegate:instance]) + webViewIdentifier:webViewIdentifier + completion:completion]; +} +@end + +@implementation FWFNavigationDelegate +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [super initWithBinaryMessenger:binaryMessenger instanceManager:instanceManager]; + if (self) { + _navigationDelegateAPI = + [[FWFNavigationDelegateFlutterApiImpl alloc] initWithBinaryMessenger:binaryMessenger + instanceManager:instanceManager]; + } + return self; +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + [self.navigationDelegateAPI didFinishNavigationForDelegate:self + webView:webView + URL:webView.URL.absoluteString + completion:^(NSError *error) { + NSAssert(!error, @"%@", error); + }]; +} + +- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { + [self.navigationDelegateAPI didStartProvisionalNavigationForDelegate:self + webView:webView + URL:webView.URL.absoluteString + completion:^(NSError *error) { + NSAssert(!error, @"%@", error); + }]; +} + +- (void)webView:(WKWebView *)webView + decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction + decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + [self.navigationDelegateAPI + decidePolicyForNavigationActionForDelegate:self + webView:webView + navigationAction:navigationAction + completion:^(FWFWKNavigationActionPolicyEnumData *policy, + NSError *error) { + NSAssert(!error, @"%@", error); + decisionHandler( + FWFWKNavigationActionPolicyFromEnumData(policy)); + }]; +} + +- (void)webView:(WKWebView *)webView + didFailNavigation:(WKNavigation *)navigation + withError:(NSError *)error { + [self.navigationDelegateAPI didFailNavigationForDelegate:self + webView:webView + error:error + completion:^(NSError *error) { + NSAssert(!error, @"%@", error); + }]; +} + +- (void)webView:(WKWebView *)webView + didFailProvisionalNavigation:(WKNavigation *)navigation + withError:(NSError *)error { + [self.navigationDelegateAPI didFailProvisionalNavigationForDelegate:self + webView:webView + error:error + completion:^(NSError *error) { + NSAssert(!error, @"%@", error); + }]; +} + +- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { + [self.navigationDelegateAPI webViewWebContentProcessDidTerminateForDelegate:self + webView:webView + completion:^(NSError *error) { + NSAssert(!error, @"%@", error); + }]; +} +@end + +@interface FWFNavigationDelegateHostApiImpl () +// BinaryMessenger must be weak to prevent a circular reference with the host API it +// references. +@property(nonatomic, weak) id binaryMessenger; +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFNavigationDelegateHostApiImpl +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _binaryMessenger = binaryMessenger; + _instanceManager = instanceManager; + } + return self; +} + +- (FWFNavigationDelegate *)navigationDelegateForIdentifier:(NSNumber *)identifier { + return (FWFNavigationDelegate *)[self.instanceManager instanceForIdentifier:identifier.longValue]; +} + +- (void)createWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + FWFNavigationDelegate *navigationDelegate = + [[FWFNavigationDelegate alloc] initWithBinaryMessenger:self.binaryMessenger + instanceManager:self.instanceManager]; + [self.instanceManager addDartCreatedInstance:navigationDelegate + withIdentifier:identifier.longValue]; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.h new file mode 100644 index 000000000000..0b740a524cef --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.h @@ -0,0 +1,47 @@ +// 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 "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Flutter api implementation for NSObject. + * + * Handles making callbacks to Dart for an NSObject. + */ +@interface FWFObjectFlutterApiImpl : FWFNSObjectFlutterApi +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; + +- (void)observeValueForObject:(NSObject *)instance + keyPath:(NSString *)keyPath + object:(NSObject *)object + change:(NSDictionary *)change + completion:(void (^)(NSError *_Nullable))completion; +@end + +/** + * Implementation of NSObject for FWFObjectHostApiImpl. + */ +@interface FWFObject : NSObject +@property(readonly, nonnull, nonatomic) FWFObjectFlutterApiImpl *objectApi; + +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +/** + * Host api implementation for NSObject. + * + * Handles creating NSObject that intercommunicate with a paired Dart object. + */ +@interface FWFObjectHostApiImpl : NSObject +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.m new file mode 100644 index 000000000000..c88b2f4e56cb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.m @@ -0,0 +1,123 @@ +// 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 "FWFObjectHostApi.h" +#import "FWFDataConverters.h" + +@interface FWFObjectFlutterApiImpl () +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFObjectFlutterApiImpl +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self initWithBinaryMessenger:binaryMessenger]; + if (self) { + _instanceManager = instanceManager; + } + return self; +} + +- (long)identifierForObject:(NSObject *)instance { + return [self.instanceManager identifierWithStrongReferenceForInstance:instance]; +} + +- (void)observeValueForObject:(NSObject *)instance + keyPath:(NSString *)keyPath + object:(NSObject *)object + change:(NSDictionary *)change + completion:(void (^)(NSError *_Nullable))completion { + NSMutableArray *changeKeys = [NSMutableArray array]; + NSMutableArray *changeValues = [NSMutableArray array]; + + [change enumerateKeysAndObjectsUsingBlock:^(NSKeyValueChangeKey key, id value, BOOL *stop) { + [changeKeys addObject:FWFNSKeyValueChangeKeyEnumDataFromNSKeyValueChangeKey(key)]; + [changeValues addObject:value]; + }]; + + NSNumber *objectIdentifier = + @([self.instanceManager identifierWithStrongReferenceForInstance:object]); + [self observeValueForObjectWithIdentifier:@([self identifierForObject:instance]) + keyPath:keyPath + objectIdentifier:objectIdentifier + changeKeys:changeKeys + changeValues:changeValues + completion:completion]; +} +@end + +@implementation FWFObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _objectApi = [[FWFObjectFlutterApiImpl alloc] initWithBinaryMessenger:binaryMessenger + instanceManager:instanceManager]; + } + return self; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + [self.objectApi observeValueForObject:self + keyPath:keyPath + object:object + change:change + completion:^(NSError *error) { + NSAssert(!error, @"%@", error); + }]; +} +@end + +@interface FWFObjectHostApiImpl () +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFObjectHostApiImpl +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _instanceManager = instanceManager; + } + return self; +} + +- (NSObject *)objectForIdentifier:(NSNumber *)identifier { + return (NSObject *)[self.instanceManager instanceForIdentifier:identifier.longValue]; +} + +- (void)addObserverForObjectWithIdentifier:(nonnull NSNumber *)identifier + observerIdentifier:(nonnull NSNumber *)observer + keyPath:(nonnull NSString *)keyPath + options: + (nonnull NSArray *) + options + error:(FlutterError *_Nullable *_Nonnull)error { + NSKeyValueObservingOptions optionsInt = 0; + for (FWFNSKeyValueObservingOptionsEnumData *data in options) { + optionsInt |= FWFNSKeyValueObservingOptionsFromEnumData(data); + } + [[self objectForIdentifier:identifier] addObserver:[self objectForIdentifier:observer] + forKeyPath:keyPath + options:optionsInt + context:nil]; +} + +- (void)removeObserverForObjectWithIdentifier:(nonnull NSNumber *)identifier + observerIdentifier:(nonnull NSNumber *)observer + keyPath:(nonnull NSString *)keyPath + error:(FlutterError *_Nullable *_Nonnull)error { + [[self objectForIdentifier:identifier] removeObserver:[self objectForIdentifier:observer] + forKeyPath:keyPath]; +} + +- (void)disposeObjectWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error { + [self.instanceManager removeInstanceWithIdentifier:identifier.longValue]; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFPreferencesHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFPreferencesHostApi.h new file mode 100644 index 000000000000..de2d26491a58 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFPreferencesHostApi.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#import "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Host api implementation for WKPreferences. + * + * Handles creating WKPreferences that intercommunicate with a paired Dart object. + */ +@interface FWFPreferencesHostApiImpl : NSObject +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFPreferencesHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFPreferencesHostApi.m new file mode 100644 index 000000000000..1a10c08eec4a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFPreferencesHostApi.m @@ -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 "FWFPreferencesHostApi.h" +#import "FWFWebViewConfigurationHostApi.h" + +@interface FWFPreferencesHostApiImpl () +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFPreferencesHostApiImpl +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _instanceManager = instanceManager; + } + return self; +} + +- (WKPreferences *)preferencesForIdentifier:(NSNumber *)identifier { + return (WKPreferences *)[self.instanceManager instanceForIdentifier:identifier.longValue]; +} + +- (void)createWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error { + WKPreferences *preferences = [[WKPreferences alloc] init]; + [self.instanceManager addDartCreatedInstance:preferences withIdentifier:identifier.longValue]; +} + +- (void)createFromWebViewConfigurationWithIdentifier:(nonnull NSNumber *)identifier + configurationIdentifier:(nonnull NSNumber *)configurationIdentifier + error:(FlutterError *_Nullable *_Nonnull)error { + WKWebViewConfiguration *configuration = (WKWebViewConfiguration *)[self.instanceManager + instanceForIdentifier:configurationIdentifier.longValue]; + [self.instanceManager addDartCreatedInstance:configuration.preferences + withIdentifier:identifier.longValue]; +} + +- (void)setJavaScriptEnabledForPreferencesWithIdentifier:(nonnull NSNumber *)identifier + isEnabled:(nonnull NSNumber *)enabled + error:(FlutterError *_Nullable *_Nonnull)error { + [[self preferencesForIdentifier:identifier] setJavaScriptEnabled:enabled.boolValue]; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.h new file mode 100644 index 000000000000..9c5769e4658b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.h @@ -0,0 +1,45 @@ +// 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 "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" +#import "FWFObjectHostApi.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Flutter api implementation for WKScriptMessageHandler. + * + * Handles making callbacks to Dart for a WKScriptMessageHandler. + */ +@interface FWFScriptMessageHandlerFlutterApiImpl : FWFWKScriptMessageHandlerFlutterApi +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +/** + * Implementation of WKScriptMessageHandler for FWFScriptMessageHandlerHostApiImpl. + */ +@interface FWFScriptMessageHandler : FWFObject +@property(readonly, nonnull, nonatomic) + FWFScriptMessageHandlerFlutterApiImpl *scriptMessageHandlerAPI; + +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +/** + * Host api implementation for WKScriptMessageHandler. + * + * Handles creating WKScriptMessageHandler that intercommunicate with a paired Dart object. + */ +@interface FWFScriptMessageHandlerHostApiImpl : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.m new file mode 100644 index 000000000000..d9e8b934a79a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.m @@ -0,0 +1,96 @@ +// 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 "FWFScriptMessageHandlerHostApi.h" +#import "FWFDataConverters.h" + +@interface FWFScriptMessageHandlerFlutterApiImpl () +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFScriptMessageHandlerFlutterApiImpl +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self initWithBinaryMessenger:binaryMessenger]; + if (self) { + _instanceManager = instanceManager; + } + return self; +} + +- (long)identifierForHandler:(FWFScriptMessageHandler *)instance { + return [self.instanceManager identifierWithStrongReferenceForInstance:instance]; +} + +- (void)didReceiveScriptMessageForHandler:(FWFScriptMessageHandler *)instance + userContentController:(WKUserContentController *)userContentController + message:(WKScriptMessage *)message + completion:(void (^)(NSError *_Nullable))completion { + NSNumber *userContentControllerIdentifier = + @([self.instanceManager identifierWithStrongReferenceForInstance:userContentController]); + FWFWKScriptMessageData *messageData = FWFWKScriptMessageDataFromWKScriptMessage(message); + [self didReceiveScriptMessageForHandlerWithIdentifier:@([self identifierForHandler:instance]) + userContentControllerIdentifier:userContentControllerIdentifier + message:messageData + completion:completion]; +} +@end + +@implementation FWFScriptMessageHandler +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [super initWithBinaryMessenger:binaryMessenger instanceManager:instanceManager]; + if (self) { + _scriptMessageHandlerAPI = + [[FWFScriptMessageHandlerFlutterApiImpl alloc] initWithBinaryMessenger:binaryMessenger + instanceManager:instanceManager]; + } + return self; +} + +- (void)userContentController:(nonnull WKUserContentController *)userContentController + didReceiveScriptMessage:(nonnull WKScriptMessage *)message { + [self.scriptMessageHandlerAPI didReceiveScriptMessageForHandler:self + userContentController:userContentController + message:message + completion:^(NSError *error) { + NSAssert(!error, @"%@", error); + }]; +} +@end + +@interface FWFScriptMessageHandlerHostApiImpl () +// BinaryMessenger must be weak to prevent a circular reference with the host API it +// references. +@property(nonatomic, weak) id binaryMessenger; +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFScriptMessageHandlerHostApiImpl +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _binaryMessenger = binaryMessenger; + _instanceManager = instanceManager; + } + return self; +} + +- (FWFScriptMessageHandler *)scriptMessageHandlerForIdentifier:(NSNumber *)identifier { + return (FWFScriptMessageHandler *)[self.instanceManager + instanceForIdentifier:identifier.longValue]; +} + +- (void)createWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error { + FWFScriptMessageHandler *scriptMessageHandler = + [[FWFScriptMessageHandler alloc] initWithBinaryMessenger:self.binaryMessenger + instanceManager:self.instanceManager]; + [self.instanceManager addDartCreatedInstance:scriptMessageHandler + withIdentifier:identifier.longValue]; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScrollViewHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScrollViewHostApi.h new file mode 100644 index 000000000000..25f373f374e3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScrollViewHostApi.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#import "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Host api implementation for UIScrollView. + * + * Handles creating UIScrollView that intercommunicate with a paired Dart object. + */ +@interface FWFScrollViewHostApiImpl : NSObject +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScrollViewHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScrollViewHostApi.m new file mode 100644 index 000000000000..a32e9565b514 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScrollViewHostApi.m @@ -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. + +#import "FWFScrollViewHostApi.h" +#import "FWFWebViewHostApi.h" + +@interface FWFScrollViewHostApiImpl () +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFScrollViewHostApiImpl +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _instanceManager = instanceManager; + } + return self; +} + +- (UIScrollView *)scrollViewForIdentifier:(NSNumber *)identifier { + return (UIScrollView *)[self.instanceManager instanceForIdentifier:identifier.longValue]; +} + +- (void)createFromWebViewWithIdentifier:(nonnull NSNumber *)identifier + webViewIdentifier:(nonnull NSNumber *)webViewIdentifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + WKWebView *webView = + (WKWebView *)[self.instanceManager instanceForIdentifier:webViewIdentifier.longValue]; + [self.instanceManager addDartCreatedInstance:webView.scrollView + withIdentifier:identifier.longValue]; +} + +- (NSArray *) + contentOffsetForScrollViewWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error { + CGPoint point = [[self scrollViewForIdentifier:identifier] contentOffset]; + return @[ @(point.x), @(point.y) ]; +} + +- (void)scrollByForScrollViewWithIdentifier:(nonnull NSNumber *)identifier + x:(nonnull NSNumber *)x + y:(nonnull NSNumber *)y + error:(FlutterError *_Nullable *_Nonnull)error { + UIScrollView *scrollView = [self scrollViewForIdentifier:identifier]; + CGPoint contentOffset = scrollView.contentOffset; + [scrollView setContentOffset:CGPointMake(contentOffset.x + x.doubleValue, + contentOffset.y + y.doubleValue)]; +} + +- (void)setContentOffsetForScrollViewWithIdentifier:(nonnull NSNumber *)identifier + toX:(nonnull NSNumber *)x + y:(nonnull NSNumber *)y + error:(FlutterError *_Nullable *_Nonnull)error { + [[self scrollViewForIdentifier:identifier] + setContentOffset:CGPointMake(x.doubleValue, y.doubleValue)]; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.h new file mode 100644 index 000000000000..7b6b4eec9b8e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.h @@ -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. + +#import +#import + +#import "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" +#import "FWFObjectHostApi.h" +#import "FWFWebViewConfigurationHostApi.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Flutter api implementation for WKUIDelegate. + * + * Handles making callbacks to Dart for a WKUIDelegate. + */ +@interface FWFUIDelegateFlutterApiImpl : FWFWKUIDelegateFlutterApi +@property(readonly, nonatomic) + FWFWebViewConfigurationFlutterApiImpl *webViewConfigurationFlutterApi; + +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +/** + * Implementation of WKUIDelegate for FWFUIDelegateHostApiImpl. + */ +@interface FWFUIDelegate : FWFObject +@property(readonly, nonnull, nonatomic) FWFUIDelegateFlutterApiImpl *UIDelegateAPI; + +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +/** + * Host api implementation for WKUIDelegate. + * + * Handles creating WKUIDelegate that intercommunicate with a paired Dart object. + */ +@interface FWFUIDelegateHostApiImpl : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m new file mode 100644 index 000000000000..60e7ad11965c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m @@ -0,0 +1,116 @@ +// 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 "FWFUIDelegateHostApi.h" +#import "FWFDataConverters.h" + +@interface FWFUIDelegateFlutterApiImpl () +// BinaryMessenger must be weak to prevent a circular reference with the host API it +// references. +@property(nonatomic, weak) id binaryMessenger; +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFUIDelegateFlutterApiImpl +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self initWithBinaryMessenger:binaryMessenger]; + if (self) { + _binaryMessenger = binaryMessenger; + _instanceManager = instanceManager; + _webViewConfigurationFlutterApi = + [[FWFWebViewConfigurationFlutterApiImpl alloc] initWithBinaryMessenger:binaryMessenger + instanceManager:instanceManager]; + } + return self; +} + +- (long)identifierForDelegate:(FWFUIDelegate *)instance { + return [self.instanceManager identifierWithStrongReferenceForInstance:instance]; +} + +- (void)onCreateWebViewForDelegate:(FWFUIDelegate *)instance + webView:(WKWebView *)webView + configuration:(WKWebViewConfiguration *)configuration + navigationAction:(WKNavigationAction *)navigationAction + completion:(void (^)(NSError *_Nullable))completion { + if (![self.instanceManager containsInstance:configuration]) { + [self.webViewConfigurationFlutterApi createWithConfiguration:configuration + completion:^(NSError *error) { + NSAssert(!error, @"%@", error); + }]; + } + + NSNumber *configurationIdentifier = + @([self.instanceManager identifierWithStrongReferenceForInstance:configuration]); + FWFWKNavigationActionData *navigationActionData = + FWFWKNavigationActionDataFromNavigationAction(navigationAction); + + [self onCreateWebViewForDelegateWithIdentifier:@([self identifierForDelegate:instance]) + webViewIdentifier: + @([self.instanceManager + identifierWithStrongReferenceForInstance:webView]) + configurationIdentifier:configurationIdentifier + navigationAction:navigationActionData + completion:completion]; +} +@end + +@implementation FWFUIDelegate +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [super initWithBinaryMessenger:binaryMessenger instanceManager:instanceManager]; + if (self) { + _UIDelegateAPI = [[FWFUIDelegateFlutterApiImpl alloc] initWithBinaryMessenger:binaryMessenger + instanceManager:instanceManager]; + } + return self; +} + +- (WKWebView *)webView:(WKWebView *)webView + createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration + forNavigationAction:(WKNavigationAction *)navigationAction + windowFeatures:(WKWindowFeatures *)windowFeatures { + [self.UIDelegateAPI onCreateWebViewForDelegate:self + webView:webView + configuration:configuration + navigationAction:navigationAction + completion:^(NSError *error) { + NSAssert(!error, @"%@", error); + }]; + return nil; +} +@end + +@interface FWFUIDelegateHostApiImpl () +// BinaryMessenger must be weak to prevent a circular reference with the host API it +// references. +@property(nonatomic, weak) id binaryMessenger; +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFUIDelegateHostApiImpl +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _binaryMessenger = binaryMessenger; + _instanceManager = instanceManager; + } + return self; +} + +- (FWFUIDelegate *)delegateForIdentifier:(NSNumber *)identifier { + return (FWFUIDelegate *)[self.instanceManager instanceForIdentifier:identifier.longValue]; +} + +- (void)createWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error { + FWFUIDelegate *uIDelegate = [[FWFUIDelegate alloc] initWithBinaryMessenger:self.binaryMessenger + instanceManager:self.instanceManager]; + [self.instanceManager addDartCreatedInstance:uIDelegate withIdentifier:identifier.longValue]; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIViewHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIViewHostApi.h new file mode 100644 index 000000000000..82edd6b742ca --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIViewHostApi.h @@ -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 + +#import "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Host api implementation for UIView. + * + * Handles creating UIView that intercommunicate with a paired Dart object. + */ +@interface FWFUIViewHostApiImpl : NSObject +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIViewHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIViewHostApi.m new file mode 100644 index 000000000000..a990561c4fba --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIViewHostApi.m @@ -0,0 +1,44 @@ +// 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 "FWFUIViewHostApi.h" + +@interface FWFUIViewHostApiImpl () +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFUIViewHostApiImpl +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _instanceManager = instanceManager; + } + return self; +} + +- (UIView *)viewForIdentifier:(NSNumber *)identifier { + return (UIView *)[self.instanceManager instanceForIdentifier:identifier.longValue]; +} + +- (void)setBackgroundColorForViewWithIdentifier:(nonnull NSNumber *)identifier + toValue:(nullable NSNumber *)color + error:(FlutterError *_Nullable *_Nonnull)error { + if (!color) { + [[self viewForIdentifier:identifier] setBackgroundColor:nil]; + } + int colorInt = color.intValue; + UIColor *colorObject = [UIColor colorWithRed:(colorInt >> 16 & 0xff) / 255.0 + green:(colorInt >> 8 & 0xff) / 255.0 + blue:(colorInt & 0xff) / 255.0 + alpha:(colorInt >> 24 & 0xff) / 255.0]; + [[self viewForIdentifier:identifier] setBackgroundColor:colorObject]; +} + +- (void)setOpaqueForViewWithIdentifier:(nonnull NSNumber *)identifier + isOpaque:(nonnull NSNumber *)opaque + error:(FlutterError *_Nullable *_Nonnull)error { + [[self viewForIdentifier:identifier] setOpaque:opaque.boolValue]; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.h new file mode 100644 index 000000000000..f0e5a1383ac3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#import "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Host api implementation for WKUserContentController. + * + * Handles creating WKUserContentController that intercommunicate with a paired Dart object. + */ +@interface FWFUserContentControllerHostApiImpl : NSObject +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.m new file mode 100644 index 000000000000..08bbaa68c99c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.m @@ -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 "FWFUserContentControllerHostApi.h" +#import "FWFDataConverters.h" +#import "FWFWebViewConfigurationHostApi.h" + +@interface FWFUserContentControllerHostApiImpl () +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFUserContentControllerHostApiImpl +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _instanceManager = instanceManager; + } + return self; +} + +- (WKUserContentController *)userContentControllerForIdentifier:(NSNumber *)identifier { + return (WKUserContentController *)[self.instanceManager + instanceForIdentifier:identifier.longValue]; +} + +- (void)createFromWebViewConfigurationWithIdentifier:(nonnull NSNumber *)identifier + configurationIdentifier:(nonnull NSNumber *)configurationIdentifier + error:(FlutterError *_Nullable *_Nonnull)error { + WKWebViewConfiguration *configuration = (WKWebViewConfiguration *)[self.instanceManager + instanceForIdentifier:configurationIdentifier.longValue]; + [self.instanceManager addDartCreatedInstance:configuration.userContentController + withIdentifier:identifier.longValue]; +} + +- (void)addScriptMessageHandlerForControllerWithIdentifier:(nonnull NSNumber *)identifier + handlerIdentifier:(nonnull NSNumber *)handler + ofName:(nonnull NSString *)name + error: + (FlutterError *_Nullable *_Nonnull)error { + [[self userContentControllerForIdentifier:identifier] + addScriptMessageHandler:(id)[self.instanceManager + instanceForIdentifier:handler.longValue] + name:name]; +} + +- (void)removeScriptMessageHandlerForControllerWithIdentifier:(nonnull NSNumber *)identifier + name:(nonnull NSString *)name + error:(FlutterError *_Nullable *_Nonnull) + error { + [[self userContentControllerForIdentifier:identifier] removeScriptMessageHandlerForName:name]; +} + +- (void)removeAllScriptMessageHandlersForControllerWithIdentifier:(nonnull NSNumber *)identifier + error: + (FlutterError *_Nullable *_Nonnull) + error { + if (@available(iOS 14.0, *)) { + [[self userContentControllerForIdentifier:identifier] removeAllScriptMessageHandlers]; + } else { + *error = [FlutterError + errorWithCode:@"FWFUnsupportedVersionError" + message:@"removeAllScriptMessageHandlers is only supported on versions 14+." + details:nil]; + } +} + +- (void)addUserScriptForControllerWithIdentifier:(nonnull NSNumber *)identifier + userScript:(nonnull FWFWKUserScriptData *)userScript + error:(FlutterError *_Nullable *_Nonnull)error { + [[self userContentControllerForIdentifier:identifier] + addUserScript:FWFWKUserScriptFromScriptData(userScript)]; +} + +- (void)removeAllUserScriptsForControllerWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error { + [[self userContentControllerForIdentifier:identifier] removeAllUserScripts]; +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.h new file mode 100644 index 000000000000..f1e62cc0cba3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.h @@ -0,0 +1,47 @@ +// 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 "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" +#import "FWFObjectHostApi.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Flutter api implementation for WKWebViewConfiguration. + * + * Handles making callbacks to Dart for a WKWebViewConfiguration. + */ +@interface FWFWebViewConfigurationFlutterApiImpl : FWFWKWebViewConfigurationFlutterApi +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; + +- (void)createWithConfiguration:(WKWebViewConfiguration *)configuration + completion:(void (^)(NSError *_Nullable))completion; +@end + +/** + * Implementation of WKWebViewConfiguration for FWFWebViewConfigurationHostApiImpl. + */ +@interface FWFWebViewConfiguration : WKWebViewConfiguration +@property(readonly, nonnull, nonatomic) FWFObjectFlutterApiImpl *objectApi; + +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +/** + * Host api implementation for WKWebViewConfiguration. + * + * Handles creating WKWebViewConfiguration that intercommunicate with a paired Dart object. + */ +@interface FWFWebViewConfigurationHostApiImpl : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m new file mode 100644 index 000000000000..a083a2a031ef --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m @@ -0,0 +1,144 @@ +// 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 "FWFWebViewConfigurationHostApi.h" +#import "FWFDataConverters.h" +#import "FWFWebViewConfigurationHostApi.h" + +@interface FWFWebViewConfigurationFlutterApiImpl () +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFWebViewConfigurationFlutterApiImpl +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self initWithBinaryMessenger:binaryMessenger]; + if (self) { + _instanceManager = instanceManager; + } + return self; +} + +- (void)createWithConfiguration:(WKWebViewConfiguration *)configuration + completion:(void (^)(NSError *_Nullable))completion { + long identifier = [self.instanceManager addHostCreatedInstance:configuration]; + [self createWithIdentifier:@(identifier) completion:completion]; +} +@end + +@implementation FWFWebViewConfiguration +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _objectApi = [[FWFObjectFlutterApiImpl alloc] initWithBinaryMessenger:binaryMessenger + instanceManager:instanceManager]; + } + return self; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + [self.objectApi observeValueForObject:self + keyPath:keyPath + object:object + change:change + completion:^(NSError *error) { + NSAssert(!error, @"%@", error); + }]; +} +@end + +@interface FWFWebViewConfigurationHostApiImpl () +// BinaryMessenger must be weak to prevent a circular reference with the host API it +// references. +@property(nonatomic, weak) id binaryMessenger; +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFWebViewConfigurationHostApiImpl +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _binaryMessenger = binaryMessenger; + _instanceManager = instanceManager; + } + return self; +} + +- (WKWebViewConfiguration *)webViewConfigurationForIdentifier:(NSNumber *)identifier { + return (WKWebViewConfiguration *)[self.instanceManager + instanceForIdentifier:identifier.longValue]; +} + +- (void)createWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable *_Nonnull)error { + FWFWebViewConfiguration *webViewConfiguration = + [[FWFWebViewConfiguration alloc] initWithBinaryMessenger:self.binaryMessenger + instanceManager:self.instanceManager]; + [self.instanceManager addDartCreatedInstance:webViewConfiguration + withIdentifier:identifier.longValue]; +} + +- (void)createFromWebViewWithIdentifier:(nonnull NSNumber *)identifier + webViewIdentifier:(nonnull NSNumber *)webViewIdentifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + WKWebView *webView = + (WKWebView *)[self.instanceManager instanceForIdentifier:webViewIdentifier.longValue]; + [self.instanceManager addDartCreatedInstance:webView.configuration + withIdentifier:identifier.longValue]; +} + +- (void)setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:(nonnull NSNumber *)identifier + isAllowed:(nonnull NSNumber *)allow + error: + (FlutterError *_Nullable *_Nonnull) + error { + [[self webViewConfigurationForIdentifier:identifier] + setAllowsInlineMediaPlayback:allow.boolValue]; +} + +- (void) + setMediaTypesRequiresUserActionForConfigurationWithIdentifier:(nonnull NSNumber *)identifier + forTypes: + (nonnull NSArray< + FWFWKAudiovisualMediaTypeEnumData + *> *)types + error: + (FlutterError *_Nullable *_Nonnull) + error { + NSAssert(types.count, @"Types must not be empty."); + + WKWebViewConfiguration *configuration = + (WKWebViewConfiguration *)[self webViewConfigurationForIdentifier:identifier]; + if (@available(iOS 10.0, *)) { + WKAudiovisualMediaTypes typesInt = 0; + for (FWFWKAudiovisualMediaTypeEnumData *data in types) { + typesInt |= FWFWKAudiovisualMediaTypeFromEnumData(data); + } + [configuration setMediaTypesRequiringUserActionForPlayback:typesInt]; + } else { + for (FWFWKAudiovisualMediaTypeEnumData *data in types) { + switch (data.value) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + case FWFWKAudiovisualMediaTypeEnumNone: + configuration.requiresUserActionForMediaPlayback = false; + break; + case FWFWKAudiovisualMediaTypeEnumAudio: + case FWFWKAudiovisualMediaTypeEnumVideo: + case FWFWKAudiovisualMediaTypeEnumAll: + configuration.requiresUserActionForMediaPlayback = true; + break; +#pragma clang diagnostic pop + } + } + } +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.h new file mode 100644 index 000000000000..f1bb59bcb9ae --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.h @@ -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. + +#import +#import + +#import "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" +#import "FWFObjectHostApi.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * A set of Flutter and Dart assets used by a `FlutterEngine` to initialize execution. + * + * Default implementation delegates methods to FlutterDartProject. + */ +@interface FWFAssetManager : NSObject +- (NSString *)lookupKeyForAsset:(NSString *)asset; +@end + +/** + * Implementation of WKWebView that can be used as a FlutterPlatformView. + */ +@interface FWFWebView : WKWebView +@property(readonly, nonnull, nonatomic) FWFObjectFlutterApiImpl *objectApi; + +- (instancetype)initWithFrame:(CGRect)frame + configuration:(nonnull WKWebViewConfiguration *)configuration + binaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; +@end + +/** + * Host api implementation for WKWebView. + * + * Handles creating WKWebViews that intercommunicate with a paired Dart object. + */ +@interface FWFWebViewHostApiImpl : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager; + +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager + bundle:(NSBundle *)bundle + assetManager:(FWFAssetManager *)assetManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m new file mode 100644 index 000000000000..ceaa346c8747 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m @@ -0,0 +1,290 @@ +// 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 "FWFWebViewHostApi.h" +#import "FWFDataConverters.h" + +@implementation FWFAssetManager +- (NSString *)lookupKeyForAsset:(NSString *)asset { + return [FlutterDartProject lookupKeyForAsset:asset]; +} +@end + +@implementation FWFWebView +- (instancetype)initWithFrame:(CGRect)frame + configuration:(nonnull WKWebViewConfiguration *)configuration + binaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + self = [self initWithFrame:frame configuration:configuration]; + if (self) { + _objectApi = [[FWFObjectFlutterApiImpl alloc] initWithBinaryMessenger:binaryMessenger + instanceManager:instanceManager]; + if (@available(iOS 11.0, *)) { + self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + if (@available(iOS 13.0, *)) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = NO; + } + } + } + return self; +} + +- (void)setFrame:(CGRect)frame { + [super setFrame:frame]; + // Prevents the contentInsets from being adjusted by iOS and gives control to Flutter. + self.scrollView.contentInset = UIEdgeInsetsZero; + if (@available(iOS 11, *)) { + // Above iOS 11, adjust contentInset to compensate the adjustedContentInset so the sum will + // always be 0. + if (UIEdgeInsetsEqualToEdgeInsets(self.scrollView.adjustedContentInset, UIEdgeInsetsZero)) { + return; + } + UIEdgeInsets insetToAdjust = self.scrollView.adjustedContentInset; + self.scrollView.contentInset = UIEdgeInsetsMake(-insetToAdjust.top, -insetToAdjust.left, + -insetToAdjust.bottom, -insetToAdjust.right); + } +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + [self.objectApi observeValueForObject:self + keyPath:keyPath + object:object + change:change + completion:^(NSError *error) { + NSAssert(!error, @"%@", error); + }]; +} + +- (nonnull UIView *)view { + return self; +} +@end + +@interface FWFWebViewHostApiImpl () +// BinaryMessenger must be weak to prevent a circular reference with the host API it +// references. +@property(nonatomic, weak) id binaryMessenger; +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@property NSBundle *bundle; +@property FWFAssetManager *assetManager; +@end + +@implementation FWFWebViewHostApiImpl +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager { + return [self initWithBinaryMessenger:binaryMessenger + instanceManager:instanceManager + bundle:[NSBundle mainBundle] + assetManager:[[FWFAssetManager alloc] init]]; +} + +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger + instanceManager:(FWFInstanceManager *)instanceManager + bundle:(NSBundle *)bundle + assetManager:(FWFAssetManager *)assetManager { + self = [self init]; + if (self) { + _binaryMessenger = binaryMessenger; + _instanceManager = instanceManager; + _bundle = bundle; + _assetManager = assetManager; + } + return self; +} + +- (FWFWebView *)webViewForIdentifier:(NSNumber *)identifier { + return (FWFWebView *)[self.instanceManager instanceForIdentifier:identifier.longValue]; +} + ++ (nonnull FlutterError *)errorForURLString:(nonnull NSString *)string { + NSString *errorDetails = [NSString stringWithFormat:@"Initializing NSURL with the supplied " + @"'%@' path resulted in a nil value.", + string]; + return [FlutterError errorWithCode:@"FWFURLParsingError" + message:@"Failed parsing file path." + details:errorDetails]; +} + +- (void)createWithIdentifier:(nonnull NSNumber *)identifier + configurationIdentifier:(nonnull NSNumber *)configurationIdentifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + WKWebViewConfiguration *configuration = (WKWebViewConfiguration *)[self.instanceManager + instanceForIdentifier:configurationIdentifier.longValue]; + FWFWebView *webView = [[FWFWebView alloc] initWithFrame:CGRectMake(0, 0, 0, 0) + configuration:configuration + binaryMessenger:self.binaryMessenger + instanceManager:self.instanceManager]; + [self.instanceManager addDartCreatedInstance:webView withIdentifier:identifier.longValue]; +} + +- (void)loadRequestForWebViewWithIdentifier:(nonnull NSNumber *)identifier + request:(nonnull FWFNSUrlRequestData *)request + error: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + NSURLRequest *urlRequest = FWFNSURLRequestFromRequestData(request); + if (!urlRequest) { + *error = [FlutterError errorWithCode:@"FWFURLRequestParsingError" + message:@"Failed instantiating an NSURLRequest." + details:[NSString stringWithFormat:@"URL was: '%@'", request.url]]; + return; + } + [[self webViewForIdentifier:identifier] loadRequest:urlRequest]; +} + +- (void)setUserAgentForWebViewWithIdentifier:(nonnull NSNumber *)identifier + userAgent:(nullable NSString *)userAgent + error:(FlutterError *_Nullable __autoreleasing *_Nonnull) + error { + [[self webViewForIdentifier:identifier] setCustomUserAgent:userAgent]; +} + +- (nullable NSNumber *) + canGoBackForWebViewWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return @([self webViewForIdentifier:identifier].canGoBack); +} + +- (nullable NSString *) + URLForWebViewWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return [self webViewForIdentifier:identifier].URL.absoluteString; +} + +- (nullable NSNumber *) + canGoForwardForWebViewWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return @([[self webViewForIdentifier:identifier] canGoForward]); +} + +- (nullable NSNumber *) + estimatedProgressForWebViewWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull) + error { + return @([[self webViewForIdentifier:identifier] estimatedProgress]); +} + +- (void)evaluateJavaScriptForWebViewWithIdentifier:(nonnull NSNumber *)identifier + javaScriptString:(nonnull NSString *)javaScriptString + completion: + (nonnull void (^)(id _Nullable, + FlutterError *_Nullable))completion { + [[self webViewForIdentifier:identifier] + evaluateJavaScript:javaScriptString + completionHandler:^(id _Nullable result, NSError *_Nullable error) { + id returnValue = nil; + FlutterError *flutterError = nil; + if (!error) { + if (!result || [result isKindOfClass:[NSString class]] || + [result isKindOfClass:[NSNumber class]]) { + returnValue = result; + } else if (![result isKindOfClass:[NSNull class]]) { + NSString *className = NSStringFromClass([result class]); + NSLog(@"Return type of evaluateJavaScript is not directly supported: %@. Returned " + @"description of value.", + className); + returnValue = [result description]; + } + } else { + flutterError = [FlutterError errorWithCode:@"FWFEvaluateJavaScriptError" + message:@"Failed evaluating JavaScript." + details:FWFNSErrorDataFromNSError(error)]; + } + + completion(returnValue, flutterError); + }]; +} + +- (void)goBackForWebViewWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [[self webViewForIdentifier:identifier] goBack]; +} + +- (void)goForwardForWebViewWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [[self webViewForIdentifier:identifier] goForward]; +} + +- (void)loadAssetForWebViewWithIdentifier:(nonnull NSNumber *)identifier + assetKey:(nonnull NSString *)key + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + NSString *assetFilePath = [self.assetManager lookupKeyForAsset:key]; + + NSURL *url = [self.bundle URLForResource:[assetFilePath stringByDeletingPathExtension] + withExtension:assetFilePath.pathExtension]; + if (!url) { + *error = [FWFWebViewHostApiImpl errorForURLString:assetFilePath]; + } else { + [[self webViewForIdentifier:identifier] loadFileURL:url + allowingReadAccessToURL:[url URLByDeletingLastPathComponent]]; + } +} + +- (void)loadFileForWebViewWithIdentifier:(nonnull NSNumber *)identifier + fileURL:(nonnull NSString *)url + readAccessURL:(nonnull NSString *)readAccessUrl + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + NSURL *fileURL = [NSURL fileURLWithPath:url isDirectory:NO]; + NSURL *readAccessNSURL = [NSURL fileURLWithPath:readAccessUrl isDirectory:YES]; + + if (!fileURL) { + *error = [FWFWebViewHostApiImpl errorForURLString:url]; + } else if (!readAccessNSURL) { + *error = [FWFWebViewHostApiImpl errorForURLString:readAccessUrl]; + } else { + [[self webViewForIdentifier:identifier] loadFileURL:fileURL + allowingReadAccessToURL:readAccessNSURL]; + } +} + +- (void)loadHTMLForWebViewWithIdentifier:(nonnull NSNumber *)identifier + HTMLString:(nonnull NSString *)string + baseURL:(nullable NSString *)baseUrl + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [[self webViewForIdentifier:identifier] loadHTMLString:string + baseURL:[NSURL URLWithString:baseUrl]]; +} + +- (void)reloadWebViewWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [[self webViewForIdentifier:identifier] reload]; +} + +- (void) + setAllowsBackForwardForWebViewWithIdentifier:(nonnull NSNumber *)identifier + isAllowed:(nonnull NSNumber *)allow + error:(FlutterError *_Nullable __autoreleasing *_Nonnull) + error { + [[self webViewForIdentifier:identifier] setAllowsBackForwardNavigationGestures:allow.boolValue]; +} + +- (void) + setNavigationDelegateForWebViewWithIdentifier:(nonnull NSNumber *)identifier + delegateIdentifier:(nullable NSNumber *)navigationDelegateIdentifier + error: + (FlutterError *_Nullable __autoreleasing *_Nonnull) + error { + id navigationDelegate = (id)[self.instanceManager + instanceForIdentifier:navigationDelegateIdentifier.longValue]; + [[self webViewForIdentifier:identifier] setNavigationDelegate:navigationDelegate]; +} + +- (void)setUIDelegateForWebViewWithIdentifier:(nonnull NSNumber *)identifier + delegateIdentifier:(nullable NSNumber *)uiDelegateIdentifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull) + error { + id navigationDelegate = + (id)[self.instanceManager instanceForIdentifier:uiDelegateIdentifier.longValue]; + [[self webViewForIdentifier:identifier] setUIDelegate:navigationDelegate]; +} + +- (nullable NSString *) + titleForWebViewWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return [[self webViewForIdentifier:identifier] title]; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.h new file mode 100644 index 000000000000..72f00e032ee4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#import "FWFGeneratedWebKitApis.h" +#import "FWFInstanceManager.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Host api implementation for WKWebsiteDataStore. + * + * Handles creating WKWebsiteDataStore that intercommunicate with a paired Dart object. + */ +@interface FWFWebsiteDataStoreHostApiImpl : NSObject +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.m new file mode 100644 index 000000000000..5398d14d4e8b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.m @@ -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. + +#import "FWFWebsiteDataStoreHostApi.h" +#import "FWFDataConverters.h" +#import "FWFWebViewConfigurationHostApi.h" + +@interface FWFWebsiteDataStoreHostApiImpl () +// InstanceManager must be weak to prevent a circular reference with the object it stores. +@property(nonatomic, weak) FWFInstanceManager *instanceManager; +@end + +@implementation FWFWebsiteDataStoreHostApiImpl +- (instancetype)initWithInstanceManager:(FWFInstanceManager *)instanceManager { + self = [self init]; + if (self) { + _instanceManager = instanceManager; + } + return self; +} + +- (WKWebsiteDataStore *)websiteDataStoreForIdentifier:(NSNumber *)identifier { + return (WKWebsiteDataStore *)[self.instanceManager instanceForIdentifier:identifier.longValue]; +} + +- (void)createFromWebViewConfigurationWithIdentifier:(nonnull NSNumber *)identifier + configurationIdentifier:(nonnull NSNumber *)configurationIdentifier + error:(FlutterError *_Nullable *_Nonnull)error { + WKWebViewConfiguration *configuration = (WKWebViewConfiguration *)[self.instanceManager + instanceForIdentifier:configurationIdentifier.longValue]; + [self.instanceManager addDartCreatedInstance:configuration.websiteDataStore + withIdentifier:identifier.longValue]; +} + +- (void)createDefaultDataStoreWithIdentifier:(nonnull NSNumber *)identifier + error:(FlutterError *_Nullable __autoreleasing *_Nonnull) + error { + [self.instanceManager addDartCreatedInstance:[WKWebsiteDataStore defaultDataStore] + withIdentifier:identifier.longValue]; +} + +- (void) + removeDataFromDataStoreWithIdentifier:(nonnull NSNumber *)identifier + ofTypes: + (nonnull NSArray *)dataTypes + modifiedSince:(nonnull NSNumber *)modificationTimeInSecondsSinceEpoch + completion:(nonnull void (^)(NSNumber *_Nullable, + FlutterError *_Nullable))completion { + NSMutableSet *stringDataTypes = [NSMutableSet set]; + for (FWFWKWebsiteDataTypeEnumData *type in dataTypes) { + [stringDataTypes addObject:FWFWKWebsiteDataTypeFromEnumData(type)]; + } + + WKWebsiteDataStore *dataStore = [self websiteDataStoreForIdentifier:identifier]; + [dataStore + fetchDataRecordsOfTypes:stringDataTypes + completionHandler:^(NSArray *records) { + [dataStore + removeDataOfTypes:stringDataTypes + modifiedSince:[NSDate dateWithTimeIntervalSince1970: + modificationTimeInSecondsSinceEpoch.doubleValue] + completionHandler:^{ + completion([NSNumber numberWithBool:(records.count > 0)], nil); + }]; + }]; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h deleted file mode 100644 index 69a15fc063c8..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h +++ /dev/null @@ -1,40 +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 -#import - -#import "FLTCookieManager.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * The WkWebView used for the plugin. - * - * This class overrides some methods in `WKWebView` to serve the needs for the plugin. - */ -@interface FLTWKWebView : WKWebView -@end - -@interface FLTWebViewController : NSObject - -@property(nonatomic) FLTWKWebView *webView; - -- (instancetype)initWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args - binaryMessenger:(NSObject *)messenger; - -- (UIView *)view; - -- (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result; - -@end - -@interface FLTWebViewFactory : NSObject -- (instancetype)initWithMessenger:(NSObject *)messenger - cookieManager:(FLTCookieManager *)cookieManager; -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap index 096507557688..1b7eaf646ee9 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap @@ -5,7 +5,6 @@ framework module webview_flutter_wkwebview { module * { export * } explicit module Test { - header "FlutterWebView_Test.h" - header "FLTCookieManager_Test.h" + header "FWFInstanceManager_Test.h" } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h deleted file mode 100644 index f442d7af8d87..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h +++ /dev/null @@ -1,17 +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 -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FLTJavaScriptChannel : NSObject - -- (instancetype)initWithMethodChannel:(FlutterMethodChannel *)methodChannel - javaScriptChannelName:(NSString *)javaScriptChannelName; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m deleted file mode 100644 index 1aed25f1b7d9..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m +++ /dev/null @@ -1,36 +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 "JavaScriptChannelHandler.h" - -@implementation FLTJavaScriptChannel { - FlutterMethodChannel *_methodChannel; - NSString *_javaScriptChannelName; -} - -- (instancetype)initWithMethodChannel:(FlutterMethodChannel *)methodChannel - javaScriptChannelName:(NSString *)javaScriptChannelName { - self = [super init]; - NSAssert(methodChannel != nil, @"methodChannel must not be null."); - NSAssert(javaScriptChannelName != nil, @"javaScriptChannelName must not be null."); - if (self) { - _methodChannel = methodChannel; - _javaScriptChannelName = javaScriptChannelName; - } - return self; -} - -- (void)userContentController:(WKUserContentController *)userContentController - didReceiveScriptMessage:(WKScriptMessage *)message { - NSAssert(_methodChannel != nil, @"Can't send a message to an unitialized JavaScript channel."); - NSAssert(_javaScriptChannelName != nil, - @"Can't send a message to an unitialized JavaScript channel."); - NSDictionary *arguments = @{ - @"channel" : _javaScriptChannelName, - @"message" : [NSString stringWithFormat:@"%@", message.body] - }; - [_methodChannel invokeMethod:@"javascriptChannelMessage" arguments:arguments]; -} - -@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 bd0b85bd59dc..269097a814ed 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 @@ -3,9 +3,19 @@ // found in the LICENSE file. #import -#import -#import -#import #import -#import -#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/instance_manager.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/instance_manager.dart new file mode 100644 index 000000000000..3cc100aebd46 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/instance_manager.dart @@ -0,0 +1,198 @@ +// 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'; + +/// An immutable object that can provide functional copies of itself. +/// +/// All implementers are expected to be immutable as defined by the annotation. +@immutable +mixin Copyable { + /// Instantiates and returns a functionally identical object to oneself. + /// + /// Outside of tests, this method should only ever be called by + /// [InstanceManager]. + /// + /// Subclasses should always override their parent's implementation of this + /// method. + @protected + Copyable copy(); +} + +/// Maintains instances used to communicate with the native objects they +/// represent. +/// +/// Added instances are stored as weak references and their copies are stored +/// as strong references to maintain access to their variables and callback +/// methods. Both are stored with the same identifier. +/// +/// When a weak referenced instance becomes inaccessible, +/// [onWeakReferenceRemoved] is called with its associated identifier. +/// +/// If an instance is retrieved and has the possibility to be used, +/// (e.g. calling [getInstanceWithWeakReference]) a copy of the strong reference +/// is added as a weak reference with the same identifier. This prevents a +/// scenario where the weak referenced instance was released and then later +/// returned by the host platform. +class InstanceManager { + /// Constructs an [InstanceManager]. + InstanceManager({required void Function(int) onWeakReferenceRemoved}) { + this.onWeakReferenceRemoved = (int identifier) { + _weakInstances.remove(identifier); + onWeakReferenceRemoved(identifier); + }; + _finalizer = Finalizer(this.onWeakReferenceRemoved); + } + + // Identifiers are locked to a specific range to avoid collisions with objects + // created simultaneously by the host platform. + // Host uses identifiers >= 2^16 and Dart is expected to use values n where, + // 0 <= n < 2^16. + static const int _maxDartCreatedIdentifier = 65536; + + // Expando is used because it doesn't prevent its keys from becoming + // inaccessible. This allows the manager to efficiently retrieve an identifier + // of an instance without holding a strong reference to that instance. + // + // It also doesn't use `==` to search for identifiers, which would lead to an + // infinite loop when comparing an object to its copy. (i.e. which was caused + // by calling instanceManager.getIdentifier() inside of `==` while this was a + // HashMap). + final Expando _identifiers = Expando(); + final Map> _weakInstances = + >{}; + final Map _strongInstances = {}; + late final Finalizer _finalizer; + int _nextIdentifier = 0; + + /// Called when a weak referenced instance is removed by [removeWeakReference] + /// or becomes inaccessible. + late final void Function(int) onWeakReferenceRemoved; + + /// Adds a new instance that was instantiated by Dart. + /// + /// In other words, Dart wants to add a new instance that will represent + /// an object that will be instantiated on the host platform. + /// + /// Throws assertion error if the instance has already been added. + /// + /// Returns the randomly generated id of the [instance] added. + int addDartCreatedInstance(Copyable instance) { + assert(getIdentifier(instance) == null); + + final int identifier = _nextUniqueIdentifier(); + _addInstanceWithIdentifier(instance, identifier); + return identifier; + } + + /// Removes the instance, if present, and call [onWeakReferenceRemoved] with + /// its identifier. + /// + /// Returns the identifier associated with the removed instance. Otherwise, + /// `null` if the instance was not found in this manager. + /// + /// This does not remove the the strong referenced instance associated with + /// [instance]. This can be done with [remove]. + int? removeWeakReference(Copyable instance) { + final int? identifier = getIdentifier(instance); + if (identifier == null) { + return null; + } + + _identifiers[instance] = null; + _finalizer.detach(instance); + onWeakReferenceRemoved(identifier); + + return identifier; + } + + /// Removes [identifier] and its associated strongly referenced instance, if + /// present, from the manager. + /// + /// Returns the strong referenced instance associated with [identifier] before + /// it was removed. Returns `null` if [identifier] was not associated with + /// any strong reference. + /// + /// This does not remove the the weak referenced instance associtated with + /// [identifier]. This can be done with [removeWeakReference]. + T? remove(int identifier) { + return _strongInstances.remove(identifier) as T?; + } + + /// Retrieves the instance associated with identifier. + /// + /// The value returned is chosen from the following order: + /// + /// 1. A weakly referenced instance associated with identifier. + /// 2. If the only instance associated with identifier is a strongly + /// referenced instance, a copy of the instance is added as a weak reference + /// with the same identifier. Returning the newly created copy. + /// 3. If no instance is associated with identifier, returns null. + /// + /// This method also expects the host `InstanceManager` to have a strong + /// reference to the instance the identifier is associated with. + T? getInstanceWithWeakReference(int identifier) { + final Copyable? weakInstance = _weakInstances[identifier]?.target; + + if (weakInstance == null) { + final Copyable? strongInstance = _strongInstances[identifier]; + if (strongInstance != null) { + final Copyable copy = strongInstance.copy(); + _identifiers[copy] = identifier; + _weakInstances[identifier] = WeakReference(copy); + _finalizer.attach(copy, identifier, detach: copy); + return copy as T; + } + return strongInstance as T?; + } + + return weakInstance as T; + } + + /// Retrieves the identifier associated with instance. + int? getIdentifier(Copyable instance) { + return _identifiers[instance]; + } + + /// Adds a new instance that was instantiated by the host platform. + /// + /// In other words, the host platform wants to add a new instance that + /// represents an object on the host platform. Stored with [identifier]. + /// + /// Throws assertion error if the instance or its identifier has already been + /// added. + /// + /// Returns unique identifier of the [instance] added. + void addHostCreatedInstance(Copyable instance, int identifier) { + assert(!containsIdentifier(identifier)); + assert(getIdentifier(instance) == null); + assert(identifier >= 0); + _addInstanceWithIdentifier(instance, identifier); + } + + void _addInstanceWithIdentifier(Copyable instance, int identifier) { + _identifiers[instance] = identifier; + _weakInstances[identifier] = WeakReference(instance); + _finalizer.attach(instance, identifier, detach: instance); + + final Copyable copy = instance.copy(); + _identifiers[copy] = identifier; + _strongInstances[identifier] = copy; + } + + /// Whether this manager contains the given [identifier]. + bool containsIdentifier(int identifier) { + return _weakInstances.containsKey(identifier) || + _strongInstances.containsKey(identifier); + } + + int _nextUniqueIdentifier() { + late int identifier; + do { + identifier = _nextIdentifier; + _nextIdentifier = (_nextIdentifier + 1) % _maxDartCreatedIdentifier; + } while (containsIdentifier(identifier)); + return identifier; + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/weak_reference_utils.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/weak_reference_utils.dart new file mode 100644 index 000000000000..ad0c9ebf4f5c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/weak_reference_utils.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. + +/// Helper method for creating callbacks methods with a weak reference. +/// +/// Example: +/// ``` +/// final JavascriptChannelRegistry javascriptChannelRegistry = ... +/// +/// final WKScriptMessageHandler handler = WKScriptMessageHandler( +/// didReceiveScriptMessage: withWeakRefenceTo( +/// javascriptChannelRegistry, +/// (WeakReference weakReference) { +/// return ( +/// WKUserContentController userContentController, +/// WKScriptMessage message, +/// ) { +/// weakReference.target?.onJavascriptChannelMessage( +/// message.name, +/// message.body!.toString(), +/// ); +/// }; +/// }, +/// ), +/// ); +/// ``` +S withWeakRefenceTo( + T reference, + S Function(WeakReference weakReference) onCreate, +) { + final WeakReference weakReference = WeakReference(reference); + return onCreate(weakReference); +} 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.pigeon.dart new file mode 100644 index 000000000000..54bb3015af64 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.pigeon.dart @@ -0,0 +1,2618 @@ +// 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.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name +// @dart = 2.12 +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'; + +enum NSKeyValueObservingOptionsEnum { + newValue, + oldValue, + initialValue, + priorNotification, +} + +enum NSKeyValueChangeEnum { + setting, + insertion, + removal, + replacement, +} + +enum NSKeyValueChangeKeyEnum { + indexes, + kind, + newValue, + notificationIsPrior, + oldValue, +} + +enum WKUserScriptInjectionTimeEnum { + atDocumentStart, + atDocumentEnd, +} + +enum WKAudiovisualMediaTypeEnum { + none, + audio, + video, + all, +} + +enum WKWebsiteDataTypeEnum { + cookies, + memoryCache, + diskCache, + offlineWebApplicationCache, + localStorage, + sessionStorage, + webSQLDatabases, + indexedDBDatabases, +} + +enum WKNavigationActionPolicyEnum { + allow, + cancel, +} + +enum NSHttpCookiePropertyKeyEnum { + comment, + commentUrl, + discard, + domain, + expires, + maximumAge, + name, + originUrl, + path, + port, + sameSitePolicy, + secure, + value, + version, +} + +class NSKeyValueObservingOptionsEnumData { + NSKeyValueObservingOptionsEnumData({ + required this.value, + }); + + NSKeyValueObservingOptionsEnum value; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['value'] = value.index; + return pigeonMap; + } + + static NSKeyValueObservingOptionsEnumData decode(Object message) { + final Map pigeonMap = message as Map; + return NSKeyValueObservingOptionsEnumData( + value: NSKeyValueObservingOptionsEnum.values[pigeonMap['value']! as int], + ); + } +} + +class NSKeyValueChangeKeyEnumData { + NSKeyValueChangeKeyEnumData({ + required this.value, + }); + + NSKeyValueChangeKeyEnum value; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['value'] = value.index; + return pigeonMap; + } + + static NSKeyValueChangeKeyEnumData decode(Object message) { + final Map pigeonMap = message as Map; + return NSKeyValueChangeKeyEnumData( + value: NSKeyValueChangeKeyEnum.values[pigeonMap['value']! as int], + ); + } +} + +class WKUserScriptInjectionTimeEnumData { + WKUserScriptInjectionTimeEnumData({ + required this.value, + }); + + WKUserScriptInjectionTimeEnum value; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['value'] = value.index; + return pigeonMap; + } + + static WKUserScriptInjectionTimeEnumData decode(Object message) { + final Map pigeonMap = message as Map; + return WKUserScriptInjectionTimeEnumData( + value: WKUserScriptInjectionTimeEnum.values[pigeonMap['value']! as int], + ); + } +} + +class WKAudiovisualMediaTypeEnumData { + WKAudiovisualMediaTypeEnumData({ + required this.value, + }); + + WKAudiovisualMediaTypeEnum value; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['value'] = value.index; + return pigeonMap; + } + + static WKAudiovisualMediaTypeEnumData decode(Object message) { + final Map pigeonMap = message as Map; + return WKAudiovisualMediaTypeEnumData( + value: WKAudiovisualMediaTypeEnum.values[pigeonMap['value']! as int], + ); + } +} + +class WKWebsiteDataTypeEnumData { + WKWebsiteDataTypeEnumData({ + required this.value, + }); + + WKWebsiteDataTypeEnum value; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['value'] = value.index; + return pigeonMap; + } + + static WKWebsiteDataTypeEnumData decode(Object message) { + final Map pigeonMap = message as Map; + return WKWebsiteDataTypeEnumData( + value: WKWebsiteDataTypeEnum.values[pigeonMap['value']! as int], + ); + } +} + +class WKNavigationActionPolicyEnumData { + WKNavigationActionPolicyEnumData({ + required this.value, + }); + + WKNavigationActionPolicyEnum value; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['value'] = value.index; + return pigeonMap; + } + + static WKNavigationActionPolicyEnumData decode(Object message) { + final Map pigeonMap = message as Map; + return WKNavigationActionPolicyEnumData( + value: WKNavigationActionPolicyEnum.values[pigeonMap['value']! as int], + ); + } +} + +class NSHttpCookiePropertyKeyEnumData { + NSHttpCookiePropertyKeyEnumData({ + required this.value, + }); + + NSHttpCookiePropertyKeyEnum value; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['value'] = value.index; + return pigeonMap; + } + + static NSHttpCookiePropertyKeyEnumData decode(Object message) { + final Map pigeonMap = message as Map; + return NSHttpCookiePropertyKeyEnumData( + value: NSHttpCookiePropertyKeyEnum.values[pigeonMap['value']! as int], + ); + } +} + +class NSUrlRequestData { + NSUrlRequestData({ + required this.url, + this.httpMethod, + this.httpBody, + required this.allHttpHeaderFields, + }); + + String url; + String? httpMethod; + Uint8List? httpBody; + Map allHttpHeaderFields; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['url'] = url; + pigeonMap['httpMethod'] = httpMethod; + pigeonMap['httpBody'] = httpBody; + pigeonMap['allHttpHeaderFields'] = allHttpHeaderFields; + return pigeonMap; + } + + static NSUrlRequestData decode(Object message) { + final Map pigeonMap = message as Map; + return NSUrlRequestData( + url: pigeonMap['url']! as String, + httpMethod: pigeonMap['httpMethod'] as String?, + httpBody: pigeonMap['httpBody'] as Uint8List?, + allHttpHeaderFields: + (pigeonMap['allHttpHeaderFields'] as Map?)! + .cast(), + ); + } +} + +class WKUserScriptData { + WKUserScriptData({ + required this.source, + this.injectionTime, + required this.isMainFrameOnly, + }); + + String source; + WKUserScriptInjectionTimeEnumData? injectionTime; + bool isMainFrameOnly; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['source'] = source; + pigeonMap['injectionTime'] = injectionTime?.encode(); + pigeonMap['isMainFrameOnly'] = isMainFrameOnly; + return pigeonMap; + } + + static WKUserScriptData decode(Object message) { + final Map pigeonMap = message as Map; + return WKUserScriptData( + source: pigeonMap['source']! as String, + injectionTime: pigeonMap['injectionTime'] != null + ? WKUserScriptInjectionTimeEnumData.decode( + pigeonMap['injectionTime']!) + : null, + isMainFrameOnly: pigeonMap['isMainFrameOnly']! as bool, + ); + } +} + +class WKNavigationActionData { + WKNavigationActionData({ + required this.request, + required this.targetFrame, + }); + + NSUrlRequestData request; + WKFrameInfoData targetFrame; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['request'] = request.encode(); + pigeonMap['targetFrame'] = targetFrame.encode(); + return pigeonMap; + } + + static WKNavigationActionData decode(Object message) { + final Map pigeonMap = message as Map; + return WKNavigationActionData( + request: NSUrlRequestData.decode(pigeonMap['request']!), + targetFrame: WKFrameInfoData.decode(pigeonMap['targetFrame']!), + ); + } +} + +class WKFrameInfoData { + WKFrameInfoData({ + required this.isMainFrame, + }); + + bool isMainFrame; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['isMainFrame'] = isMainFrame; + return pigeonMap; + } + + static WKFrameInfoData decode(Object message) { + final Map pigeonMap = message as Map; + return WKFrameInfoData( + isMainFrame: pigeonMap['isMainFrame']! as bool, + ); + } +} + +class NSErrorData { + NSErrorData({ + required this.code, + required this.domain, + required this.localizedDescription, + }); + + int code; + String domain; + String localizedDescription; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['code'] = code; + pigeonMap['domain'] = domain; + pigeonMap['localizedDescription'] = localizedDescription; + return pigeonMap; + } + + static NSErrorData decode(Object message) { + final Map pigeonMap = message as Map; + return NSErrorData( + code: pigeonMap['code']! as int, + domain: pigeonMap['domain']! as String, + localizedDescription: pigeonMap['localizedDescription']! as String, + ); + } +} + +class WKScriptMessageData { + WKScriptMessageData({ + required this.name, + this.body, + }); + + String name; + Object? body; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['name'] = name; + pigeonMap['body'] = body; + return pigeonMap; + } + + static WKScriptMessageData decode(Object message) { + final Map pigeonMap = message as Map; + return WKScriptMessageData( + name: pigeonMap['name']! as String, + body: pigeonMap['body'] as Object?, + ); + } +} + +class NSHttpCookieData { + NSHttpCookieData({ + required this.propertyKeys, + required this.propertyValues, + }); + + List propertyKeys; + List propertyValues; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['propertyKeys'] = propertyKeys; + pigeonMap['propertyValues'] = propertyValues; + return pigeonMap; + } + + static NSHttpCookieData decode(Object message) { + final Map pigeonMap = message as Map; + return NSHttpCookieData( + propertyKeys: (pigeonMap['propertyKeys'] as List?)! + .cast(), + propertyValues: + (pigeonMap['propertyValues'] as List?)!.cast(), + ); + } +} + +class _WKWebsiteDataStoreHostApiCodec extends StandardMessageCodec { + const _WKWebsiteDataStoreHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WKWebsiteDataTypeEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WKWebsiteDataTypeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class WKWebsiteDataStoreHostApi { + /// Constructor for [WKWebsiteDataStoreHostApi]. 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. + WKWebsiteDataStoreHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WKWebsiteDataStoreHostApiCodec(); + + Future createFromWebViewConfiguration( + int arg_identifier, int arg_configurationIdentifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_configurationIdentifier]) + 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 createDefaultDataStore(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createDefaultDataStore', + 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; + } + } + + Future removeDataOfTypes( + int arg_identifier, + List arg_dataTypes, + double arg_modificationTimeInSecondsSinceEpoch) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel.send([ + arg_identifier, + arg_dataTypes, + arg_modificationTimeInSecondsSinceEpoch + ]) 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 bool?)!; + } + } +} + +class _UIViewHostApiCodec extends StandardMessageCodec { + const _UIViewHostApiCodec(); +} + +class UIViewHostApi { + /// Constructor for [UIViewHostApi]. 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. + UIViewHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _UIViewHostApiCodec(); + + Future setBackgroundColor(int arg_identifier, int? arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIViewHostApi.setBackgroundColor', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_value]) 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 setOpaque(int arg_identifier, bool arg_opaque) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIViewHostApi.setOpaque', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_opaque]) 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 _UIScrollViewHostApiCodec extends StandardMessageCodec { + const _UIScrollViewHostApiCodec(); +} + +class UIScrollViewHostApi { + /// Constructor for [UIScrollViewHostApi]. 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. + UIScrollViewHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _UIScrollViewHostApiCodec(); + + Future createFromWebView( + int arg_identifier, int arg_webViewIdentifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.createFromWebView', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_webViewIdentifier]) + 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> getContentOffset(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.getContentOffset', 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 scrollBy(int arg_identifier, double arg_x, double arg_y) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.scrollBy', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_x, arg_y]) + 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 setContentOffset( + int arg_identifier, double arg_x, double arg_y) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_x, arg_y]) + 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 _WKWebViewConfigurationHostApiCodec extends StandardMessageCodec { + const _WKWebViewConfigurationHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WKAudiovisualMediaTypeEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WKAudiovisualMediaTypeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class WKWebViewConfigurationHostApi { + /// Constructor for [WKWebViewConfigurationHostApi]. 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. + WKWebViewConfigurationHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = + _WKWebViewConfigurationHostApiCodec(); + + Future create(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.create', 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; + } + } + + Future createFromWebView( + int arg_identifier, int arg_webViewIdentifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_webViewIdentifier]) + 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 setAllowsInlineMediaPlayback( + int arg_identifier, bool arg_allow) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_allow]) 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 setMediaTypesRequiringUserActionForPlayback(int arg_identifier, + List arg_types) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_types]) 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 _WKWebViewConfigurationFlutterApiCodec extends StandardMessageCodec { + const _WKWebViewConfigurationFlutterApiCodec(); +} + +abstract class WKWebViewConfigurationFlutterApi { + static const MessageCodec codec = + _WKWebViewConfigurationFlutterApiCodec(); + + void create(int identifier); + static void setup(WKWebViewConfigurationFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationFlutterApi.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.WKWebViewConfigurationFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + +class _WKUserContentControllerHostApiCodec extends StandardMessageCodec { + const _WKUserContentControllerHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WKUserScriptData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptInjectionTimeEnumData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WKUserScriptData.decode(readValue(buffer)!); + + case 129: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class WKUserContentControllerHostApi { + /// Constructor for [WKUserContentControllerHostApi]. 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. + WKUserContentControllerHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = + _WKUserContentControllerHostApiCodec(); + + Future createFromWebViewConfiguration( + int arg_identifier, int arg_configurationIdentifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.createFromWebViewConfiguration', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_configurationIdentifier]) + 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 addScriptMessageHandler( + int arg_identifier, int arg_handlerIdentifier, String arg_name) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_handlerIdentifier, arg_name]) + 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 removeScriptMessageHandler( + int arg_identifier, String arg_name) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeScriptMessageHandler', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_name]) 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 removeAllScriptMessageHandlers(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllScriptMessageHandlers', + 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; + } + } + + Future addUserScript( + int arg_identifier, WKUserScriptData arg_userScript) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.addUserScript', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_userScript]) + 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 removeAllUserScripts(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllUserScripts', + 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 _WKPreferencesHostApiCodec extends StandardMessageCodec { + const _WKPreferencesHostApiCodec(); +} + +class WKPreferencesHostApi { + /// Constructor for [WKPreferencesHostApi]. 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. + WKPreferencesHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WKPreferencesHostApiCodec(); + + Future createFromWebViewConfiguration( + int arg_identifier, int arg_configurationIdentifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKPreferencesHostApi.createFromWebViewConfiguration', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_configurationIdentifier]) + 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 setJavaScriptEnabled( + int arg_identifier, bool arg_enabled) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKPreferencesHostApi.setJavaScriptEnabled', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_enabled]) 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 _WKScriptMessageHandlerHostApiCodec extends StandardMessageCodec { + const _WKScriptMessageHandlerHostApiCodec(); +} + +class WKScriptMessageHandlerHostApi { + /// Constructor for [WKScriptMessageHandlerHostApi]. 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. + WKScriptMessageHandlerHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = + _WKScriptMessageHandlerHostApiCodec(); + + Future create(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKScriptMessageHandlerHostApi.create', 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 _WKScriptMessageHandlerFlutterApiCodec extends StandardMessageCodec { + const _WKScriptMessageHandlerFlutterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WKScriptMessageData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WKScriptMessageData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class WKScriptMessageHandlerFlutterApi { + static const MessageCodec codec = + _WKScriptMessageHandlerFlutterApiCodec(); + + void didReceiveScriptMessage(int identifier, + int userContentControllerIdentifier, WKScriptMessageData message); + static void setup(WKScriptMessageHandlerFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKScriptMessageHandlerFlutterApi.didReceiveScriptMessage', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKScriptMessageHandlerFlutterApi.didReceiveScriptMessage 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.WKScriptMessageHandlerFlutterApi.didReceiveScriptMessage was null, expected non-null int.'); + final int? arg_userContentControllerIdentifier = (args[1] as int?); + assert(arg_userContentControllerIdentifier != null, + 'Argument for dev.flutter.pigeon.WKScriptMessageHandlerFlutterApi.didReceiveScriptMessage was null, expected non-null int.'); + final WKScriptMessageData? arg_message = + (args[2] as WKScriptMessageData?); + assert(arg_message != null, + 'Argument for dev.flutter.pigeon.WKScriptMessageHandlerFlutterApi.didReceiveScriptMessage was null, expected non-null WKScriptMessageData.'); + api.didReceiveScriptMessage(arg_identifier!, + arg_userContentControllerIdentifier!, arg_message!); + return; + }); + } + } + } +} + +class _WKNavigationDelegateHostApiCodec extends StandardMessageCodec { + const _WKNavigationDelegateHostApiCodec(); +} + +class WKNavigationDelegateHostApi { + /// Constructor for [WKNavigationDelegateHostApi]. 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. + WKNavigationDelegateHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = + _WKNavigationDelegateHostApiCodec(); + + Future create(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKNavigationDelegateHostApi.create', 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 _WKNavigationDelegateFlutterApiCodec extends StandardMessageCodec { + const _WKNavigationDelegateFlutterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSErrorData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is NSUrlRequestData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is WKFrameInfoData) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is WKNavigationActionData) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is WKNavigationActionPolicyEnumData) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSErrorData.decode(readValue(buffer)!); + + case 129: + return NSUrlRequestData.decode(readValue(buffer)!); + + case 130: + return WKFrameInfoData.decode(readValue(buffer)!); + + case 131: + return WKNavigationActionData.decode(readValue(buffer)!); + + case 132: + return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class WKNavigationDelegateFlutterApi { + static const MessageCodec codec = + _WKNavigationDelegateFlutterApiCodec(); + + void didFinishNavigation(int identifier, int webViewIdentifier, String? url); + void didStartProvisionalNavigation( + int identifier, int webViewIdentifier, String? url); + Future decidePolicyForNavigationAction( + int identifier, + int webViewIdentifier, + WKNavigationActionData navigationAction); + void didFailNavigation( + int identifier, int webViewIdentifier, NSErrorData error); + void didFailProvisionalNavigation( + int identifier, int webViewIdentifier, NSErrorData error); + void webViewWebContentProcessDidTerminate( + int identifier, int webViewIdentifier); + static void setup(WKNavigationDelegateFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFinishNavigation', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFinishNavigation 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.WKNavigationDelegateFlutterApi.didFinishNavigation was null, expected non-null int.'); + final int? arg_webViewIdentifier = (args[1] as int?); + assert(arg_webViewIdentifier != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFinishNavigation was null, expected non-null int.'); + final String? arg_url = (args[2] as String?); + api.didFinishNavigation( + arg_identifier!, arg_webViewIdentifier!, arg_url); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didStartProvisionalNavigation', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didStartProvisionalNavigation 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.WKNavigationDelegateFlutterApi.didStartProvisionalNavigation was null, expected non-null int.'); + final int? arg_webViewIdentifier = (args[1] as int?); + assert(arg_webViewIdentifier != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didStartProvisionalNavigation was null, expected non-null int.'); + final String? arg_url = (args[2] as String?); + api.didStartProvisionalNavigation( + arg_identifier!, arg_webViewIdentifier!, arg_url); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKNavigationDelegateFlutterApi.decidePolicyForNavigationAction', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.decidePolicyForNavigationAction 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.WKNavigationDelegateFlutterApi.decidePolicyForNavigationAction was null, expected non-null int.'); + final int? arg_webViewIdentifier = (args[1] as int?); + assert(arg_webViewIdentifier != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.decidePolicyForNavigationAction was null, expected non-null int.'); + final WKNavigationActionData? arg_navigationAction = + (args[2] as WKNavigationActionData?); + assert(arg_navigationAction != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.decidePolicyForNavigationAction was null, expected non-null WKNavigationActionData.'); + final WKNavigationActionPolicyEnumData output = + await api.decidePolicyForNavigationAction(arg_identifier!, + arg_webViewIdentifier!, arg_navigationAction!); + return output; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFailNavigation', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFailNavigation 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.WKNavigationDelegateFlutterApi.didFailNavigation was null, expected non-null int.'); + final int? arg_webViewIdentifier = (args[1] as int?); + assert(arg_webViewIdentifier != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFailNavigation was null, expected non-null int.'); + final NSErrorData? arg_error = (args[2] as NSErrorData?); + assert(arg_error != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFailNavigation was null, expected non-null NSErrorData.'); + api.didFailNavigation( + arg_identifier!, arg_webViewIdentifier!, arg_error!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFailProvisionalNavigation', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFailProvisionalNavigation 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.WKNavigationDelegateFlutterApi.didFailProvisionalNavigation was null, expected non-null int.'); + final int? arg_webViewIdentifier = (args[1] as int?); + assert(arg_webViewIdentifier != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFailProvisionalNavigation was null, expected non-null int.'); + final NSErrorData? arg_error = (args[2] as NSErrorData?); + assert(arg_error != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.didFailProvisionalNavigation was null, expected non-null NSErrorData.'); + api.didFailProvisionalNavigation( + arg_identifier!, arg_webViewIdentifier!, arg_error!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKNavigationDelegateFlutterApi.webViewWebContentProcessDidTerminate', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.webViewWebContentProcessDidTerminate 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.WKNavigationDelegateFlutterApi.webViewWebContentProcessDidTerminate was null, expected non-null int.'); + final int? arg_webViewIdentifier = (args[1] as int?); + assert(arg_webViewIdentifier != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateFlutterApi.webViewWebContentProcessDidTerminate was null, expected non-null int.'); + api.webViewWebContentProcessDidTerminate( + arg_identifier!, arg_webViewIdentifier!); + return; + }); + } + } + } +} + +class _NSObjectHostApiCodec extends StandardMessageCodec { + const _NSObjectHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSKeyValueObservingOptionsEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSKeyValueObservingOptionsEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class NSObjectHostApi { + /// Constructor for [NSObjectHostApi]. 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. + NSObjectHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _NSObjectHostApiCodec(); + + Future dispose(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectHostApi.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; + } + } + + Future addObserver( + int arg_identifier, + int arg_observerIdentifier, + String arg_keyPath, + List arg_options) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectHostApi.addObserver', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel.send([ + arg_identifier, + arg_observerIdentifier, + arg_keyPath, + arg_options + ]) 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 removeObserver(int arg_identifier, int arg_observerIdentifier, + String arg_keyPath) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectHostApi.removeObserver', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel.send( + [arg_identifier, arg_observerIdentifier, arg_keyPath]) + 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 _NSObjectFlutterApiCodec extends StandardMessageCodec { + const _NSObjectFlutterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSErrorData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is NSHttpCookieData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is NSHttpCookiePropertyKeyEnumData) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is NSKeyValueChangeKeyEnumData) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is NSKeyValueObservingOptionsEnumData) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is NSUrlRequestData) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is WKAudiovisualMediaTypeEnumData) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is WKFrameInfoData) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else if (value is WKNavigationActionData) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is WKNavigationActionPolicyEnumData) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else if (value is WKScriptMessageData) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptData) { + buffer.putUint8(139); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptInjectionTimeEnumData) { + buffer.putUint8(140); + writeValue(buffer, value.encode()); + } else if (value is WKWebsiteDataTypeEnumData) { + buffer.putUint8(141); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSErrorData.decode(readValue(buffer)!); + + case 129: + return NSHttpCookieData.decode(readValue(buffer)!); + + case 130: + return NSHttpCookiePropertyKeyEnumData.decode(readValue(buffer)!); + + case 131: + return NSKeyValueChangeKeyEnumData.decode(readValue(buffer)!); + + case 132: + return NSKeyValueObservingOptionsEnumData.decode(readValue(buffer)!); + + case 133: + return NSUrlRequestData.decode(readValue(buffer)!); + + case 134: + return WKAudiovisualMediaTypeEnumData.decode(readValue(buffer)!); + + case 135: + return WKFrameInfoData.decode(readValue(buffer)!); + + case 136: + return WKNavigationActionData.decode(readValue(buffer)!); + + case 137: + return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); + + case 138: + return WKScriptMessageData.decode(readValue(buffer)!); + + case 139: + return WKUserScriptData.decode(readValue(buffer)!); + + case 140: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + + case 141: + return WKWebsiteDataTypeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class NSObjectFlutterApi { + static const MessageCodec codec = _NSObjectFlutterApiCodec(); + + void observeValue( + int identifier, + String keyPath, + int objectIdentifier, + List changeKeys, + List changeValues); + void dispose(int identifier); + static void setup(NSObjectFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectFlutterApi.observeValue', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.NSObjectFlutterApi.observeValue 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.NSObjectFlutterApi.observeValue was null, expected non-null int.'); + final String? arg_keyPath = (args[1] as String?); + assert(arg_keyPath != null, + 'Argument for dev.flutter.pigeon.NSObjectFlutterApi.observeValue was null, expected non-null String.'); + final int? arg_objectIdentifier = (args[2] as int?); + assert(arg_objectIdentifier != null, + 'Argument for dev.flutter.pigeon.NSObjectFlutterApi.observeValue was null, expected non-null int.'); + final List? arg_changeKeys = + (args[3] as List?)?.cast(); + assert(arg_changeKeys != null, + 'Argument for dev.flutter.pigeon.NSObjectFlutterApi.observeValue was null, expected non-null List.'); + final List? arg_changeValues = + (args[4] as List?)?.cast(); + assert(arg_changeValues != null, + 'Argument for dev.flutter.pigeon.NSObjectFlutterApi.observeValue was null, expected non-null List.'); + api.observeValue(arg_identifier!, arg_keyPath!, arg_objectIdentifier!, + arg_changeKeys!, arg_changeValues!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectFlutterApi.dispose', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.NSObjectFlutterApi.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.NSObjectFlutterApi.dispose was null, expected non-null int.'); + api.dispose(arg_identifier!); + return; + }); + } + } + } +} + +class _WKWebViewHostApiCodec extends StandardMessageCodec { + const _WKWebViewHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSErrorData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is NSHttpCookieData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is NSHttpCookiePropertyKeyEnumData) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is NSKeyValueChangeKeyEnumData) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is NSKeyValueObservingOptionsEnumData) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is NSUrlRequestData) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is WKAudiovisualMediaTypeEnumData) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is WKFrameInfoData) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else if (value is WKNavigationActionData) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is WKNavigationActionPolicyEnumData) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else if (value is WKScriptMessageData) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptData) { + buffer.putUint8(139); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptInjectionTimeEnumData) { + buffer.putUint8(140); + writeValue(buffer, value.encode()); + } else if (value is WKWebsiteDataTypeEnumData) { + buffer.putUint8(141); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSErrorData.decode(readValue(buffer)!); + + case 129: + return NSHttpCookieData.decode(readValue(buffer)!); + + case 130: + return NSHttpCookiePropertyKeyEnumData.decode(readValue(buffer)!); + + case 131: + return NSKeyValueChangeKeyEnumData.decode(readValue(buffer)!); + + case 132: + return NSKeyValueObservingOptionsEnumData.decode(readValue(buffer)!); + + case 133: + return NSUrlRequestData.decode(readValue(buffer)!); + + case 134: + return WKAudiovisualMediaTypeEnumData.decode(readValue(buffer)!); + + case 135: + return WKFrameInfoData.decode(readValue(buffer)!); + + case 136: + return WKNavigationActionData.decode(readValue(buffer)!); + + case 137: + return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); + + case 138: + return WKScriptMessageData.decode(readValue(buffer)!); + + case 139: + return WKUserScriptData.decode(readValue(buffer)!); + + case 140: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + + case 141: + return WKWebsiteDataTypeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class WKWebViewHostApi { + /// Constructor for [WKWebViewHostApi]. 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. + WKWebViewHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WKWebViewHostApiCodec(); + + Future create( + int arg_identifier, int arg_configurationIdentifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_configurationIdentifier]) + 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 setUIDelegate( + int arg_identifier, int? arg_uiDelegateIdentifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setUIDelegate', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_uiDelegateIdentifier]) + 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 setNavigationDelegate( + int arg_identifier, int? arg_navigationDelegateIdentifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setNavigationDelegate', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_navigationDelegateIdentifier]) + 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 getUrl(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.getUrl', 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 (replyMap['result'] as String?); + } + } + + Future getEstimatedProgress(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.getEstimatedProgress', 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 double?)!; + } + } + + Future loadRequest( + int arg_identifier, NSUrlRequestData arg_request) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadRequest', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_request]) 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 loadHtmlString( + int arg_identifier, String arg_string, String? arg_baseUrl) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadHtmlString', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_string, arg_baseUrl]) + 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 loadFileUrl( + int arg_identifier, String arg_url, String arg_readAccessUrl) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_url, arg_readAccessUrl]) + 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 loadFlutterAsset(int arg_identifier, String arg_key) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadFlutterAsset', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_key]) 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 canGoBack(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.canGoBack', 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 bool?)!; + } + } + + Future canGoForward(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.canGoForward', 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 bool?)!; + } + } + + Future goBack(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.goBack', 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; + } + } + + Future goForward(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.goForward', 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; + } + } + + Future reload(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.reload', 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; + } + } + + Future getTitle(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.getTitle', 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 (replyMap['result'] as String?); + } + } + + Future setAllowsBackForwardNavigationGestures( + int arg_identifier, bool arg_allow) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setAllowsBackForwardNavigationGestures', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_allow]) 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 setCustomUserAgent( + int arg_identifier, String? arg_userAgent) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setCustomUserAgent', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_userAgent]) + 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 evaluateJavaScript( + int arg_identifier, String arg_javaScriptString) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.evaluateJavaScript', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_javaScriptString]) + 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 Object?); + } + } +} + +class _WKUIDelegateHostApiCodec extends StandardMessageCodec { + const _WKUIDelegateHostApiCodec(); +} + +class WKUIDelegateHostApi { + /// Constructor for [WKUIDelegateHostApi]. 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. + WKUIDelegateHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WKUIDelegateHostApiCodec(); + + Future create(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUIDelegateHostApi.create', 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 _WKUIDelegateFlutterApiCodec extends StandardMessageCodec { + const _WKUIDelegateFlutterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSUrlRequestData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is WKFrameInfoData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is WKNavigationActionData) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSUrlRequestData.decode(readValue(buffer)!); + + case 129: + return WKFrameInfoData.decode(readValue(buffer)!); + + case 130: + return WKNavigationActionData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class WKUIDelegateFlutterApi { + static const MessageCodec codec = _WKUIDelegateFlutterApiCodec(); + + void onCreateWebView(int identifier, int webViewIdentifier, + int configurationIdentifier, WKNavigationActionData navigationAction); + static void setup(WKUIDelegateFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUIDelegateFlutterApi.onCreateWebView', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.onCreateWebView 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.WKUIDelegateFlutterApi.onCreateWebView was null, expected non-null int.'); + final int? arg_webViewIdentifier = (args[1] as int?); + assert(arg_webViewIdentifier != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.onCreateWebView was null, expected non-null int.'); + final int? arg_configurationIdentifier = (args[2] as int?); + assert(arg_configurationIdentifier != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.onCreateWebView was null, expected non-null int.'); + final WKNavigationActionData? arg_navigationAction = + (args[3] as WKNavigationActionData?); + assert(arg_navigationAction != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.onCreateWebView was null, expected non-null WKNavigationActionData.'); + api.onCreateWebView(arg_identifier!, arg_webViewIdentifier!, + arg_configurationIdentifier!, arg_navigationAction!); + return; + }); + } + } + } +} + +class _WKHttpCookieStoreHostApiCodec extends StandardMessageCodec { + const _WKHttpCookieStoreHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSHttpCookieData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is NSHttpCookiePropertyKeyEnumData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSHttpCookieData.decode(readValue(buffer)!); + + case 129: + return NSHttpCookiePropertyKeyEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class WKHttpCookieStoreHostApi { + /// Constructor for [WKHttpCookieStoreHostApi]. 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. + WKHttpCookieStoreHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WKHttpCookieStoreHostApiCodec(); + + Future createFromWebsiteDataStore( + int arg_identifier, int arg_websiteDataStoreIdentifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKHttpCookieStoreHostApi.createFromWebsiteDataStore', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_websiteDataStoreIdentifier]) + 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 setCookie( + int arg_identifier, NSHttpCookieData arg_cookie) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKHttpCookieStoreHostApi.setCookie', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_cookie]) 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; + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart index 9615fc5a9a51..9f121e66d5cb 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart @@ -2,9 +2,174 @@ // 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:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import '../common/instance_manager.dart'; +import '../common/weak_reference_utils.dart'; +import 'foundation_api_impls.dart'; + +/// The values that can be returned in a change map. +/// +/// Wraps [NSKeyValueObservingOptions](https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions?language=objc). +enum NSKeyValueObservingOptions { + /// Indicates that the change map should provide the new attribute value, if applicable. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions/nskeyvalueobservingoptionnew?language=objc. + newValue, + + /// Indicates that the change map should contain the old attribute value, if applicable. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions/nskeyvalueobservingoptionold?language=objc. + oldValue, + + /// Indicates a notification should be sent to the observer immediately. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions/nskeyvalueobservingoptioninitial?language=objc. + initialValue, + + /// Whether separate notifications should be sent to the observer before and after each change. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions/nskeyvalueobservingoptionprior?language=objc. + priorNotification, +} + +/// The kinds of changes that can be observed. +/// +/// Wraps [NSKeyValueChange](https://developer.apple.com/documentation/foundation/nskeyvaluechange?language=objc). +enum NSKeyValueChange { + /// Indicates that the value of the observed key path was set to a new value. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechange/nskeyvaluechangesetting?language=objc. + setting, + + /// Indicates that an object has been inserted into the to-many relationship that is being observed. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechange/nskeyvaluechangeinsertion?language=objc. + insertion, + + /// Indicates that an object has been removed from the to-many relationship that is being observed. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechange/nskeyvaluechangeremoval?language=objc. + removal, + + /// Indicates that an object has been replaced in the to-many relationship that is being observed. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechange/nskeyvaluechangereplacement?language=objc. + replacement, +} + +/// The keys that can appear in the change map. +/// +/// Wraps [NSKeyValueChangeKey](https://developer.apple.com/documentation/foundation/nskeyvaluechangekey?language=objc). +enum NSKeyValueChangeKey { + /// Indicates changes made in a collection. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechangeindexeskey?language=objc. + indexes, + + /// Indicates what sort of change has occurred. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechangekindkey?language=objc. + kind, + + /// Indicates the new value for the attribute. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechangenewkey?language=objc. + newValue, + + /// Indicates a notification is sent prior to a change. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechangenotificationispriorkey?language=objc. + notificationIsPrior, + + /// Indicates the value of this key is the value before the attribute was changed. + /// + /// See https://developer.apple.com/documentation/foundation/nskeyvaluechangeoldkey?language=objc. + oldValue, +} + +/// The supported keys in a cookie attributes dictionary. +/// +/// Wraps [NSHTTPCookiePropertyKey](https://developer.apple.com/documentation/foundation/nshttpcookiepropertykey). +enum NSHttpCookiePropertyKey { + /// A String object containing the comment for the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiecomment. + comment, + + /// A String object containing the comment URL for the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiecommenturl. + commentUrl, + + /// A String object stating whether the cookie should be discarded at the end of the session. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiediscard. + discard, + + /// A String object specifying the expiration date for the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiedomain. + domain, + + /// A String object specifying the expiration date for the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookieexpires. + expires, + + /// A String object containing an integer value stating how long in seconds the cookie should be kept, at most. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiemaximumage. + maximumAge, + + /// A String object containing the name of the cookie (required). + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiename. + name, + + /// A String object containing the URL that set this cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookieoriginurl. + originUrl, + + /// A String object containing the path for the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiepath. + path, + + /// A String object containing comma-separated integer values specifying the ports for the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookieport. + port, + + /// A String indicating the same-site policy for the cookie. + /// + /// This is only supported on iOS version 13+. This value will be ignored on + /// versions < 13. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiesamesitepolicy. + sameSitePolicy, + + /// A String object indicating that the cookie should be transmitted only over secure channels. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookiesecure. + secure, + + /// A String object containing the value of the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookievalue. + value, + + /// A String object that specifies the version of the cookie. + /// + /// See https://developer.apple.com/documentation/foundation/nshttpcookieversion. + version, +} /// A URL load request that is independent of protocol or URL scheme. /// @@ -33,3 +198,121 @@ class NSUrlRequest { /// All of the HTTP header fields for a request. final Map allHttpHeaderFields; } + +/// Information about an error condition. +/// +/// Wraps [NSError](https://developer.apple.com/documentation/foundation/nserror?language=objc). +@immutable +class NSError { + /// Constructs an [NSError]. + const NSError({ + required this.code, + required this.domain, + required this.localizedDescription, + }); + + /// The error code. + /// + /// Note that errors are domain-specific. + final int code; + + /// A string containing the error domain. + final String domain; + + /// A string containing the localized description of the error. + final String localizedDescription; +} + +/// A representation of an HTTP cookie. +/// +/// Wraps [NSHTTPCookie](https://developer.apple.com/documentation/foundation/nshttpcookie). +@immutable +class NSHttpCookie { + /// Initializes an HTTP cookie object using the provided properties. + const NSHttpCookie.withProperties(this.properties); + + /// Properties of the new cookie object. + final Map properties; +} + +/// The root class of most Objective-C class hierarchies. +@immutable +class NSObject with Copyable { + /// Constructs a [NSObject] without creating the associated + /// Objective-C object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + NSObject.detached({ + this.observeValue, + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _api = NSObjectHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ) { + // Ensures FlutterApis for the Foundation library are set up. + FoundationFlutterApis.instance.ensureSetUp(); + } + + /// Release the reference to the Objective-C object. + static void dispose(NSObject instance) { + instance._api.instanceManager.removeWeakReference(instance); + } + + /// Global instance of [InstanceManager]. + static final InstanceManager globalInstanceManager = + InstanceManager(onWeakReferenceRemoved: (int instanceId) { + NSObjectHostApiImpl().dispose(instanceId); + }); + + final NSObjectHostApiImpl _api; + + /// Informs the observing object when the value at the specified key path has + /// changed. + /// + /// {@template webview_flutter_wkwebview.foundation.callbacks} + /// For the associated Objective-C object to be automatically garbage + /// collected, it is required that this Function doesn't contain a strong + /// reference to the encapsulating class instance. Consider using + /// `WeakReference` when referencing an object not received as a parameter. + /// Otherwise, use [NSObject.dispose] to release the associated Objective-C + /// object manually. + /// + /// See [withWeakRefenceTo]. + /// {@endtemplate} + final void Function( + String keyPath, + NSObject object, + Map change, + )? observeValue; + + /// Registers the observer object to receive KVO notifications. + Future addObserver( + NSObject observer, { + required String keyPath, + required Set options, + }) { + assert(options.isNotEmpty); + return _api.addObserverForInstances( + this, + observer, + keyPath, + options, + ); + } + + /// Stops the observer object from receiving change notifications for the property. + Future removeObserver(NSObject observer, {required String keyPath}) { + return _api.removeObserverForInstances(this, observer, keyPath); + } + + @override + NSObject copy() { + return NSObject.detached( + observeValue: observeValue, + binaryMessenger: _api.binaryMessenger, + instanceManager: _api.instanceManager, + ); + } +} 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 new file mode 100644 index 000000000000..d2310e0a5df8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart @@ -0,0 +1,178 @@ +// 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/services.dart'; + +import '../common/instance_manager.dart'; +import '../common/web_kit.pigeon.dart'; +import 'foundation.dart'; + +Iterable + _toNSKeyValueObservingOptionsEnumData( + Iterable options, +) { + return options.map(( + NSKeyValueObservingOptions option, + ) { + late final NSKeyValueObservingOptionsEnum? value; + switch (option) { + case NSKeyValueObservingOptions.newValue: + value = NSKeyValueObservingOptionsEnum.newValue; + break; + case NSKeyValueObservingOptions.oldValue: + value = NSKeyValueObservingOptionsEnum.oldValue; + break; + case NSKeyValueObservingOptions.initialValue: + value = NSKeyValueObservingOptionsEnum.initialValue; + break; + case NSKeyValueObservingOptions.priorNotification: + value = NSKeyValueObservingOptionsEnum.priorNotification; + break; + } + + return NSKeyValueObservingOptionsEnumData(value: value); + }); +} + +extension _NSKeyValueChangeKeyEnumDataConverter on NSKeyValueChangeKeyEnumData { + NSKeyValueChangeKey toNSKeyValueChangeKey() { + return NSKeyValueChangeKey.values.firstWhere( + (NSKeyValueChangeKey element) => element.name == value.name, + ); + } +} + +/// Handles initialization of Flutter APIs for the Foundation library. +class FoundationFlutterApis { + /// Constructs a [FoundationFlutterApis]. + @visibleForTesting + FoundationFlutterApis({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _binaryMessenger = binaryMessenger, + object = NSObjectFlutterApiImpl( + instanceManager: instanceManager, + ); + + static FoundationFlutterApis _instance = FoundationFlutterApis(); + + /// Sets the global instance containing the Flutter Apis for the Foundation library. + @visibleForTesting + static set instance(FoundationFlutterApis instance) { + _instance = instance; + } + + /// Global instance containing the Flutter Apis for the Foundation library. + static FoundationFlutterApis get instance { + return _instance; + } + + final BinaryMessenger? _binaryMessenger; + bool _hasBeenSetUp = false; + + /// Flutter Api for [NSObject]. + @visibleForTesting + final NSObjectFlutterApiImpl object; + + /// Ensures all the Flutter APIs have been set up to receive calls from native code. + void ensureSetUp() { + if (!_hasBeenSetUp) { + NSObjectFlutterApi.setup( + object, + binaryMessenger: _binaryMessenger, + ); + _hasBeenSetUp = true; + } + } +} + +/// Host api implementation for [NSObject]. +class NSObjectHostApiImpl extends NSObjectHostApi { + /// Constructs an [NSObjectHostApiImpl]. + NSObjectHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [addObserver] with the ids of the provided object instances. + Future addObserverForInstances( + NSObject instance, + NSObject observer, + String keyPath, + Set options, + ) { + return addObserver( + instanceManager.getIdentifier(instance)!, + instanceManager.getIdentifier(observer)!, + keyPath, + _toNSKeyValueObservingOptionsEnumData(options).toList(), + ); + } + + /// Calls [removeObserver] with the ids of the provided object instances. + Future removeObserverForInstances( + NSObject instance, + NSObject observer, + String keyPath, + ) { + return removeObserver( + instanceManager.getIdentifier(instance)!, + instanceManager.getIdentifier(observer)!, + keyPath, + ); + } +} + +/// Flutter api implementation for [NSObject]. +class NSObjectFlutterApiImpl extends NSObjectFlutterApi { + /// Constructs a [NSObjectFlutterApiImpl]. + NSObjectFlutterApiImpl({InstanceManager? instanceManager}) + : instanceManager = instanceManager ?? NSObject.globalInstanceManager; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + NSObject _getObject(int identifier) { + return instanceManager.getInstanceWithWeakReference(identifier)!; + } + + @override + void observeValue( + int identifier, + String keyPath, + int objectIdentifier, + List changeKeys, + List changeValues, + ) { + final void Function(String, NSObject, Map)? + function = _getObject(identifier).observeValue; + function?.call( + keyPath, + instanceManager.getInstanceWithWeakReference(objectIdentifier)! + as NSObject, + Map.fromIterables( + changeKeys.map( + (NSKeyValueChangeKeyEnumData? data) { + return data!.toNSKeyValueChangeKey(); + }, + ), changeValues), + ); + } + + @override + void dispose(int identifier) { + instanceManager.remove(identifier); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit.dart new file mode 100644 index 000000000000..33447091e5f9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit.dart @@ -0,0 +1,137 @@ +// 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:math'; + +import 'package:flutter/foundation.dart'; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import +import 'package:flutter/painting.dart' show Color; +import 'package:flutter/services.dart'; + +import '../common/instance_manager.dart'; +import '../foundation/foundation.dart'; +import '../web_kit/web_kit.dart'; +import 'ui_kit_api_impls.dart'; + +/// A view that allows the scrolling and zooming of its contained views. +/// +/// Wraps [UIScrollView](https://developer.apple.com/documentation/uikit/uiscrollview?language=objc). +@immutable +class UIScrollView extends UIView { + /// Constructs a [UIScrollView] that is owned by [webView]. + factory UIScrollView.fromWebView( + WKWebView webView, { + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) { + final UIScrollView scrollView = UIScrollView.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + scrollView._scrollViewApi.createFromWebViewForInstances( + scrollView, + webView, + ); + return scrollView; + } + + /// Constructs a [UIScrollView] without creating the associated + /// Objective-C object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + UIScrollView.detached({ + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _scrollViewApi = UIScrollViewHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + final UIScrollViewHostApiImpl _scrollViewApi; + + /// Point at which the origin of the content view is offset from the origin of the scroll view. + /// + /// Represents [WKWebView.contentOffset](https://developer.apple.com/documentation/uikit/uiscrollview/1619404-contentoffset?language=objc). + Future> getContentOffset() { + return _scrollViewApi.getContentOffsetForInstances(this); + } + + /// Move the scrolled position of this view. + /// + /// This method is not a part of UIKit and is only a helper method to make + /// scrollBy atomic. + Future scrollBy(Point offset) { + return _scrollViewApi.scrollByForInstances(this, offset); + } + + /// Set point at which the origin of the content view is offset from the origin of the scroll view. + /// + /// The default value is `Point(0.0, 0.0)`. + /// + /// Sets [WKWebView.contentOffset](https://developer.apple.com/documentation/uikit/uiscrollview/1619404-contentoffset?language=objc). + Future setContentOffset(Point offset) { + return _scrollViewApi.setContentOffsetForInstances(this, offset); + } + + @override + UIScrollView copy() { + return UIScrollView.detached( + observeValue: observeValue, + binaryMessenger: _viewApi.binaryMessenger, + instanceManager: _viewApi.instanceManager, + ); + } +} + +/// Manages the content for a rectangular area on the screen. +/// +/// Wraps [UIView](https://developer.apple.com/documentation/uikit/uiview?language=objc). +@immutable +class UIView extends NSObject { + /// Constructs a [UIView] without creating the associated + /// Objective-C object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + UIView.detached({ + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _viewApi = UIViewHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + final UIViewHostApiImpl _viewApi; + + /// The view’s background color. + /// + /// The default value is null, which results in a transparent background color. + /// + /// Sets [UIView.backgroundColor](https://developer.apple.com/documentation/uikit/uiview/1622591-backgroundcolor?language=objc). + Future setBackgroundColor(Color? color) { + return _viewApi.setBackgroundColorForInstances(this, color); + } + + /// Determines whether the view is opaque. + /// + /// Sets [UIView.opaque](https://developer.apple.com/documentation/uikit/uiview?language=objc). + Future setOpaque(bool opaque) { + return _viewApi.setOpaqueForInstances(this, opaque); + } + + @override + UIView copy() { + return UIView.detached( + observeValue: observeValue, + binaryMessenger: _viewApi.binaryMessenger, + instanceManager: _viewApi.instanceManager, + ); + } +} 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 new file mode 100644 index 000000000000..ae12a11820d8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit_api_impls.dart @@ -0,0 +1,119 @@ +// 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:math'; + +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import +import 'package:flutter/painting.dart' show Color; +import 'package:flutter/services.dart'; + +import '../common/instance_manager.dart'; +import '../common/web_kit.pigeon.dart'; +import '../foundation/foundation.dart'; +import '../web_kit/web_kit.dart'; +import 'ui_kit.dart'; + +/// Host api implementation for [UIScrollView]. +class UIScrollViewHostApiImpl extends UIScrollViewHostApi { + /// Constructs a [UIScrollViewHostApiImpl]. + UIScrollViewHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [createFromWebView] with the ids of the provided object instances. + Future createFromWebViewForInstances( + UIScrollView instance, + WKWebView webView, + ) { + return createFromWebView( + instanceManager.addDartCreatedInstance(instance), + instanceManager.getIdentifier(webView)!, + ); + } + + /// Calls [getContentOffset] with the ids of the provided object instances. + Future> getContentOffsetForInstances( + UIScrollView instance, + ) async { + final List point = await getContentOffset( + instanceManager.getIdentifier(instance)!, + ); + return Point(point[0]!, point[1]!); + } + + /// Calls [scrollBy] with the ids of the provided object instances. + Future scrollByForInstances( + UIScrollView instance, + Point offset, + ) { + return scrollBy( + instanceManager.getIdentifier(instance)!, + offset.x, + offset.y, + ); + } + + /// Calls [setContentOffset] with the ids of the provided object instances. + Future setContentOffsetForInstances( + UIScrollView instance, + Point offset, + ) async { + return setContentOffset( + instanceManager.getIdentifier(instance)!, + offset.x, + offset.y, + ); + } +} + +/// Host api implementation for [UIView]. +class UIViewHostApiImpl extends UIViewHostApi { + /// Constructs a [UIViewHostApiImpl]. + UIViewHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [setBackgroundColor] with the ids of the provided object instances. + Future setBackgroundColorForInstances( + UIView instance, + Color? color, + ) async { + return setBackgroundColor( + instanceManager.getIdentifier(instance)!, + color?.value, + ); + } + + /// Calls [setOpaque] with the ids of the provided object instances. + Future setOpaqueForInstances( + UIView instance, + bool opaque, + ) async { + return setOpaque(instanceManager.getIdentifier(instance)!, opaque); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_proxy.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_proxy.dart new file mode 100644 index 000000000000..e3d1f609ef9c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_proxy.dart @@ -0,0 +1,73 @@ +// 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 '../../foundation/foundation.dart'; +import '../../web_kit/web_kit.dart'; + +// This convenience method was added because Dart doesn't support constant +// function literals: https://github.com/dart-lang/language/issues/1048. +WKWebsiteDataStore _defaultWebsiteDataStore() => + WKWebsiteDataStore.defaultDataStore; + +/// Handles constructing objects and calling static methods for the WebKit +/// native library. +/// +/// This class provides dependency injection for the implementations of the +/// platform interface classes. Improving the ease of unit testing and/or +/// overriding the underlying WebKit classes. +/// +/// By default each function calls the default constructor of the WebKit class +/// it intends to return. +class WebKitProxy { + /// Constructs a [WebKitProxy]. + const WebKitProxy({ + this.createWebView = WKWebView.new, + this.createWebViewConfiguration = WKWebViewConfiguration.new, + this.createScriptMessageHandler = WKScriptMessageHandler.new, + this.defaultWebsiteDataStore = _defaultWebsiteDataStore, + this.createNavigationDelegate = WKNavigationDelegate.new, + }); + + /// Constructs a [WKWebView]. + final WKWebView Function( + WKWebViewConfiguration configuration, { + void Function( + String keyPath, + NSObject object, + Map change, + )? + observeValue, + }) createWebView; + + /// Constructs a [WKWebViewConfiguration]. + final WKWebViewConfiguration Function() createWebViewConfiguration; + + /// Constructs a [WKScriptMessageHandler]. + final WKScriptMessageHandler Function({ + required void Function( + WKUserContentController userContentController, + WKScriptMessage message, + ) + didReceiveScriptMessage, + }) createScriptMessageHandler; + + /// The default [WKWebsiteDataStore]. + final WKWebsiteDataStore Function() defaultWebsiteDataStore; + + /// Constructs a [WKNavigationDelegate]. + final WKNavigationDelegate Function({ + void Function(WKWebView webView, String? url)? didFinishNavigation, + void Function(WKWebView webView, String? url)? + didStartProvisionalNavigation, + Future Function( + WKWebView webView, + WKNavigationAction navigationAction, + )? + decidePolicyForNavigationAction, + void Function(WKWebView webView, NSError error)? didFailNavigation, + void Function(WKWebView webView, NSError error)? + didFailProvisionalNavigation, + void Function(WKWebView webView)? webViewWebContentProcessDidTerminate, + }) createNavigationDelegate; +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_controller.dart new file mode 100644 index 000000000000..117aa784dc3f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_controller.dart @@ -0,0 +1,621 @@ +// 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:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as path; +import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; + +import '../../common/instance_manager.dart'; +import '../../common/weak_reference_utils.dart'; +import '../../foundation/foundation.dart'; +import '../../web_kit/web_kit.dart'; +import 'webkit_proxy.dart'; + +/// Object specifying creation parameters for a [WebKitWebViewController]. +@immutable +class WebKitWebViewControllerCreationParams + extends PlatformWebViewControllerCreationParams { + /// Constructs a [WebKitWebViewControllerCreationParams]. + WebKitWebViewControllerCreationParams({ + @visibleForTesting this.webKitProxy = const WebKitProxy(), + }) : _configuration = webKitProxy.createWebViewConfiguration(); + + /// Constructs a [WebKitWebViewControllerCreationParams] using a + /// [PlatformWebViewControllerCreationParams]. + WebKitWebViewControllerCreationParams.fromPlatformWebViewControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebViewControllerCreationParams params, { + @visibleForTesting WebKitProxy webKitProxy = const WebKitProxy(), + }) : this(webKitProxy: webKitProxy); + + final WKWebViewConfiguration _configuration; + + /// Handles constructing objects and calling static methods for the WebKit + /// native library. + @visibleForTesting + final WebKitProxy webKitProxy; +} + +/// An implementation of [PlatformWebViewController] with the WebKit api. +class WebKitWebViewController extends PlatformWebViewController { + /// Constructs a [WebKitWebViewController]. + WebKitWebViewController(PlatformWebViewControllerCreationParams params) + : super.implementation(params is WebKitWebViewControllerCreationParams + ? params + : WebKitWebViewControllerCreationParams + .fromPlatformWebViewControllerCreationParams(params)) { + _webView.addObserver( + _webView, + keyPath: 'estimatedProgress', + options: { + NSKeyValueObservingOptions.newValue, + }, + ); + } + + /// The WebKit WebView being controlled. + late final WKWebView _webView = withWeakRefenceTo(this, ( + WeakReference weakReference, + ) { + return _webKitParams.webKitProxy.createWebView( + _webKitParams._configuration, + observeValue: ( + String keyPath, + NSObject object, + Map change, + ) { + if (weakReference.target?._onProgress != null) { + final double progress = + change[NSKeyValueChangeKey.newValue]! as double; + weakReference.target!._onProgress!((progress * 100).round()); + } + }, + ); + }); + + final Map _javaScriptChannelParams = + {}; + + bool _zoomEnabled = true; + void Function(int progress)? _onProgress; + + WebKitWebViewControllerCreationParams get _webKitParams => + params as WebKitWebViewControllerCreationParams; + + @override + Future loadFile(String absoluteFilePath) { + return _webView.loadFileUrl( + absoluteFilePath, + readAccessUrl: path.dirname(absoluteFilePath), + ); + } + + @override + Future loadFlutterAsset(String key) { + assert(key.isNotEmpty); + return _webView.loadFlutterAsset(key); + } + + @override + Future loadHtmlString(String html, {String? baseUrl}) { + return _webView.loadHtmlString(html, baseUrl: baseUrl); + } + + @override + Future loadRequest(LoadRequestParams params) { + if (!params.uri.hasScheme) { + throw ArgumentError( + 'LoadRequestParams#uri is required to have a scheme.', + ); + } + + return _webView.loadRequest(NSUrlRequest( + url: params.uri.toString(), + allHttpHeaderFields: params.headers, + httpMethod: describeEnum(params.method), + httpBody: params.body, + )); + } + + @override + Future addJavaScriptChannel( + JavaScriptChannelParams javaScriptChannelParams, + ) { + final WebKitJavaScriptChannelParams webKitParams = + javaScriptChannelParams is WebKitJavaScriptChannelParams + ? javaScriptChannelParams + : WebKitJavaScriptChannelParams.fromJavaScriptChannelParams( + javaScriptChannelParams); + + _javaScriptChannelParams[webKitParams.name] = webKitParams; + + final String wrapperSource = + 'window.${webKitParams.name} = webkit.messageHandlers.${webKitParams.name};'; + final WKUserScript wrapperScript = WKUserScript( + wrapperSource, + WKUserScriptInjectionTime.atDocumentStart, + isMainFrameOnly: false, + ); + _webView.configuration.userContentController.addUserScript(wrapperScript); + return _webView.configuration.userContentController.addScriptMessageHandler( + webKitParams._messageHandler, + webKitParams.name, + ); + } + + @override + Future removeJavaScriptChannel(String javaScriptChannelName) async { + assert(javaScriptChannelName.isNotEmpty); + if (!_javaScriptChannelParams.containsKey(javaScriptChannelName)) { + return; + } + await _resetUserScripts(removedJavaScriptChannel: javaScriptChannelName); + } + + @override + Future currentUrl() => _webView.getUrl(); + + @override + Future canGoBack() => _webView.canGoBack(); + + @override + Future canGoForward() => _webView.canGoForward(); + + @override + Future goBack() => _webView.goBack(); + + @override + Future goForward() => _webView.goForward(); + + @override + Future reload() => _webView.reload(); + + @override + Future clearCache() { + return _webView.configuration.websiteDataStore.removeDataOfTypes( + { + WKWebsiteDataType.memoryCache, + WKWebsiteDataType.diskCache, + WKWebsiteDataType.offlineWebApplicationCache, + }, + DateTime.fromMillisecondsSinceEpoch(0), + ); + } + + @override + Future clearLocalStorage() { + return _webView.configuration.websiteDataStore.removeDataOfTypes( + {WKWebsiteDataType.localStorage}, + DateTime.fromMillisecondsSinceEpoch(0), + ); + } + + @override + Future runJavaScript(String javaScript) async { + try { + await _webView.evaluateJavaScript(javaScript); + } on PlatformException catch (exception) { + // WebKit will throw an error when the type of the evaluated value is + // unsupported. This also goes for `null` and `undefined` on iOS 14+. For + // example, when running a void function. For ease of use, this specific + // error is ignored when no return value is expected. + if (exception.details is! NSError || + exception.details.code != + WKErrorCode.javaScriptResultTypeIsUnsupported) { + rethrow; + } + } + } + + @override + Future runJavaScriptReturningResult(String javaScript) async { + final Object? result = await _webView.evaluateJavaScript(javaScript); + if (result == null) { + throw ArgumentError( + 'Result of JavaScript execution returned a `null` value. ' + 'Use `runJavascript` when expecting a null return value.', + ); + } + return result.toString(); + } + + /// Controls whether inline playback of HTML5 videos is allowed. + Future setAllowsInlineMediaPlayback(bool allow) { + return _webView.configuration.setAllowsInlineMediaPlayback(allow); + } + + @override + Future getTitle() => _webView.getTitle(); + + @override + Future scrollTo(int x, int y) { + return _webView.scrollView.setContentOffset(Point( + x.toDouble(), + y.toDouble(), + )); + } + + @override + Future scrollBy(int x, int y) { + return _webView.scrollView.scrollBy(Point( + x.toDouble(), + y.toDouble(), + )); + } + + @override + Future> getScrollPosition() async { + final Point offset = await _webView.scrollView.getContentOffset(); + return Point(offset.x.round(), offset.y.round()); + } + + // TODO(bparrishMines): This is unique to iOS. Override should be removed if + // this is removed from the platform interface before webview_flutter version + // 4.0.0. + @override + Future enableGestureNavigation(bool enabled) { + return _webView.setAllowsBackForwardNavigationGestures(enabled); + } + + @override + Future setBackgroundColor(Color color) { + return Future.wait(>[ + _webView.scrollView.setBackgroundColor(color), + _webView.setOpaque(false), + _webView.setBackgroundColor(Colors.transparent), + ]); + } + + @override + Future setJavaScriptMode(JavaScriptMode javaScriptMode) { + switch (javaScriptMode) { + case JavaScriptMode.disabled: + return _webView.configuration.preferences.setJavaScriptEnabled(false); + case JavaScriptMode.unrestricted: + return _webView.configuration.preferences.setJavaScriptEnabled(true); + } + } + + @override + Future setUserAgent(String? userAgent) { + return _webView.setCustomUserAgent(userAgent); + } + + @override + Future enableZoom(bool enabled) async { + if (_zoomEnabled == enabled) { + return; + } + + _zoomEnabled = enabled; + if (enabled) { + await _resetUserScripts(); + } else { + await _disableZoom(); + } + } + + @override + Future setPlatformNavigationDelegate( + covariant WebKitNavigationDelegate handler, + ) { + _onProgress = handler._onProgress; + return _webView.setNavigationDelegate(handler._navigationDelegate); + } + + Future _disableZoom() { + const WKUserScript userScript = WKUserScript( + "var meta = document.createElement('meta');\n" + "meta.name = 'viewport';\n" + "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, " + "user-scalable=no';\n" + "var head = document.getElementsByTagName('head')[0];head.appendChild(meta);", + WKUserScriptInjectionTime.atDocumentEnd, + isMainFrameOnly: true, + ); + return _webView.configuration.userContentController + .addUserScript(userScript); + } + + // WKWebView does not support removing a single user script, so all user + // scripts and all message handlers are removed instead. And the JavaScript + // channels that shouldn't be removed are re-registered. Note that this + // workaround could interfere with exposing support for custom scripts from + // applications. + Future _resetUserScripts({String? removedJavaScriptChannel}) async { + _webView.configuration.userContentController.removeAllUserScripts(); + // TODO(bparrishMines): This can be replaced with + // `removeAllScriptMessageHandlers` once Dart supports runtime version + // checking. (e.g. The equivalent to @availability in Objective-C.) + _javaScriptChannelParams.keys.forEach( + _webView.configuration.userContentController.removeScriptMessageHandler, + ); + + _javaScriptChannelParams.remove(removedJavaScriptChannel); + + await Future.wait(>[ + for (JavaScriptChannelParams params in _javaScriptChannelParams.values) + addJavaScriptChannel(params), + // Zoom is disabled with a WKUserScript, so this adds it back if it was + // removed above. + if (!_zoomEnabled) _disableZoom(), + ]); + } +} + +/// An implementation of [JavaScriptChannelParams] with the WebKit api. +/// +/// See [WebKitWebViewController.addJavaScriptChannel]. +@immutable +class WebKitJavaScriptChannelParams extends JavaScriptChannelParams { + /// Constructs a [WebKitJavaScriptChannelParams]. + WebKitJavaScriptChannelParams({ + required super.name, + required super.onMessageReceived, + @visibleForTesting WebKitProxy webKitProxy = const WebKitProxy(), + }) : assert(name.isNotEmpty), + _messageHandler = webKitProxy.createScriptMessageHandler( + didReceiveScriptMessage: withWeakRefenceTo( + onMessageReceived, + (WeakReference weakReference) { + return ( + WKUserContentController controller, + WKScriptMessage message, + ) { + if (weakReference.target != null) { + weakReference.target!( + JavaScriptMessage(message: message.body!.toString()), + ); + } + }; + }, + ), + ); + + /// Constructs a [WebKitJavaScriptChannelParams] using a + /// [JavaScriptChannelParams]. + WebKitJavaScriptChannelParams.fromJavaScriptChannelParams( + JavaScriptChannelParams params, { + @visibleForTesting WebKitProxy webKitProxy = const WebKitProxy(), + }) : this( + name: params.name, + onMessageReceived: params.onMessageReceived, + webKitProxy: webKitProxy, + ); + + final WKScriptMessageHandler _messageHandler; +} + +/// Object specifying creation parameters for a [WebKitWebViewWidget]. +@immutable +class WebKitWebViewWidgetCreationParams + extends PlatformWebViewWidgetCreationParams { + /// Constructs a [WebKitWebViewWidgetCreationParams]. + WebKitWebViewWidgetCreationParams({ + super.key, + required super.controller, + super.layoutDirection, + super.gestureRecognizers, + @visibleForTesting InstanceManager? instanceManager, + }) : _instanceManager = instanceManager ?? NSObject.globalInstanceManager; + + /// Constructs a [WebKitWebViewWidgetCreationParams] using a + /// [PlatformWebViewWidgetCreationParams]. + WebKitWebViewWidgetCreationParams.fromPlatformWebViewWidgetCreationParams( + PlatformWebViewWidgetCreationParams params, { + InstanceManager? instanceManager, + }) : this( + key: params.key, + controller: params.controller, + layoutDirection: params.layoutDirection, + gestureRecognizers: params.gestureRecognizers, + instanceManager: instanceManager, + ); + + // Maintains instances used to communicate with the native objects they + // represent. + final InstanceManager _instanceManager; +} + +/// An implementation of [PlatformWebViewWidget] with the WebKit api. +class WebKitWebViewWidget extends PlatformWebViewWidget { + /// Constructs a [WebKitWebViewWidget]. + WebKitWebViewWidget(PlatformWebViewWidgetCreationParams params) + : super.implementation( + params is WebKitWebViewWidgetCreationParams + ? params + : WebKitWebViewWidgetCreationParams + .fromPlatformWebViewWidgetCreationParams(params), + ); + + WebKitWebViewWidgetCreationParams get _webKitParams => + params as WebKitWebViewWidgetCreationParams; + + @override + Widget build(BuildContext context) { + return UiKitView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (_) {}, + layoutDirection: params.layoutDirection, + gestureRecognizers: params.gestureRecognizers, + creationParams: _webKitParams._instanceManager.getIdentifier( + (params.controller as WebKitWebViewController)._webView), + creationParamsCodec: const StandardMessageCodec(), + ); + } +} + +/// An implementation of [WebResourceError] with the WebKit API. +class WebKitWebResourceError extends WebResourceError { + WebKitWebResourceError._(this._nsError) + : super( + errorCode: _nsError.code, + description: _nsError.localizedDescription, + errorType: _toWebResourceErrorType(_nsError.code), + ); + + static WebResourceErrorType? _toWebResourceErrorType(int code) { + switch (code) { + case WKErrorCode.unknown: + return WebResourceErrorType.unknown; + case WKErrorCode.webContentProcessTerminated: + return WebResourceErrorType.webContentProcessTerminated; + case WKErrorCode.webViewInvalidated: + return WebResourceErrorType.webViewInvalidated; + case WKErrorCode.javaScriptExceptionOccurred: + return WebResourceErrorType.javaScriptExceptionOccurred; + case WKErrorCode.javaScriptResultTypeIsUnsupported: + return WebResourceErrorType.javaScriptResultTypeIsUnsupported; + } + + return null; + } + + /// A string representing the domain of the error. + String? get domain => _nsError.domain; + + final NSError _nsError; +} + +/// Object specifying creation parameters for a [WebKitNavigationDelegate]. +@immutable +class WebKitNavigationDelegateCreationParams + extends PlatformNavigationDelegateCreationParams { + /// Constructs a [WebKitNavigationDelegateCreationParams]. + const WebKitNavigationDelegateCreationParams({ + @visibleForTesting this.webKitProxy = const WebKitProxy(), + }); + + /// Constructs a [WebKitNavigationDelegateCreationParams] using a + /// [PlatformNavigationDelegateCreationParams]. + const WebKitNavigationDelegateCreationParams.fromPlatformNavigationDelegateCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformNavigationDelegateCreationParams params, { + @visibleForTesting WebKitProxy webKitProxy = const WebKitProxy(), + }) : this(webKitProxy: webKitProxy); + + /// Handles constructing objects and calling static methods for the WebKit + /// native library. + @visibleForTesting + final WebKitProxy webKitProxy; +} + +/// An implementation of [PlatformNavigationDelegate] with the WebKit API. +class WebKitNavigationDelegate extends PlatformNavigationDelegate { + /// Constructs a [WebKitNavigationDelegate]. + WebKitNavigationDelegate(PlatformNavigationDelegateCreationParams params) + : super.implementation(params is WebKitNavigationDelegateCreationParams + ? params + : WebKitNavigationDelegateCreationParams + .fromPlatformNavigationDelegateCreationParams(params)) { + final WeakReference weakThis = + WeakReference(this); + _navigationDelegate = (params as WebKitNavigationDelegateCreationParams) + .webKitProxy + .createNavigationDelegate( + didFinishNavigation: (WKWebView webView, String? url) { + if (weakThis.target?._onPageFinished != null) { + weakThis.target!._onPageFinished!(url ?? ''); + } + }, + didStartProvisionalNavigation: (WKWebView webView, String? url) { + if (weakThis.target?._onPageStarted != null) { + weakThis.target!._onPageStarted!(url ?? ''); + } + }, + decidePolicyForNavigationAction: ( + WKWebView webView, + WKNavigationAction action, + ) async { + if (weakThis.target?._onNavigationRequest != null) { + final bool allow = await weakThis.target!._onNavigationRequest!( + url: action.request.url, + isForMainFrame: action.targetFrame.isMainFrame, + ); + return allow + ? WKNavigationActionPolicy.allow + : WKNavigationActionPolicy.cancel; + } + return WKNavigationActionPolicy.allow; + }, + didFailNavigation: (WKWebView webView, NSError error) { + if (weakThis.target?._onWebResourceError != null) { + weakThis.target!._onWebResourceError!( + WebKitWebResourceError._(error), + ); + } + }, + didFailProvisionalNavigation: (WKWebView webView, NSError error) { + if (weakThis.target?._onWebResourceError != null) { + weakThis.target!._onWebResourceError!( + WebKitWebResourceError._(error), + ); + } + }, + webViewWebContentProcessDidTerminate: (WKWebView webView) { + if (weakThis.target?._onWebResourceError != null) { + weakThis.target!._onWebResourceError!( + WebKitWebResourceError._( + const NSError( + code: WKErrorCode.webContentProcessTerminated, + // Value from https://developer.apple.com/documentation/webkit/wkerrordomain?language=objc. + domain: 'WKErrorDomain', + localizedDescription: '', + ), + ), + ); + } + }, + ); + } + + // Used to set `WKWebView.setNavigationDelegate` in `WebKitWebViewController`. + late final WKNavigationDelegate _navigationDelegate; + + void Function(String url)? _onPageFinished; + void Function(String url)? _onPageStarted; + void Function(int progress)? _onProgress; + void Function(WebResourceError error)? _onWebResourceError; + FutureOr Function({required String url, required bool isForMainFrame})? + _onNavigationRequest; + + @override + Future setOnPageFinished( + void Function(String url) onPageFinished, + ) async { + _onPageFinished = onPageFinished; + } + + @override + Future setOnPageStarted(void Function(String url) onPageStarted) async { + _onPageStarted = onPageStarted; + } + + @override + Future setOnProgress(void Function(int progress) onProgress) async { + _onProgress = onProgress; + } + + @override + Future setOnWebResourceError( + void Function(WebResourceError error) onWebResourceError, + ) async { + _onWebResourceError = onWebResourceError; + } + + @override + Future setOnNavigationRequest( + FutureOr Function({required String url, required bool isForMainFrame}) + onNavigationRequest, + ) async { + _onNavigationRequest = onNavigationRequest; + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_cookie_manager.dart new file mode 100644 index 000000000000..423b3bdb7f4e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_cookie_manager.dart @@ -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. + +import 'package:flutter/foundation.dart'; +import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; + +import '../../foundation/foundation.dart'; +import '../../web_kit/web_kit.dart'; +import 'webkit_proxy.dart'; + +/// Object specifying creation parameters for a [WebKitWebViewCookieManager]. +class WebKitWebViewCookieManagerCreationParams + extends PlatformWebViewCookieManagerCreationParams { + /// Constructs a [WebKitWebViewCookieManagerCreationParams]. + WebKitWebViewCookieManagerCreationParams({ + WebKitProxy? webKitProxy, + }) : webKitProxy = webKitProxy ?? const WebKitProxy(); + + /// Constructs a [WebKitWebViewCookieManagerCreationParams] using a + /// [PlatformWebViewCookieManagerCreationParams]. + WebKitWebViewCookieManagerCreationParams.fromPlatformWebViewCookieManagerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebViewCookieManagerCreationParams params, { + @visibleForTesting WebKitProxy? webKitProxy, + }) : this(webKitProxy: webKitProxy); + + /// Handles constructing objects and calling static methods for the WebKit + /// native library. + @visibleForTesting + final WebKitProxy webKitProxy; + + /// Manages stored data for [WKWebView]s. + late final WKWebsiteDataStore _websiteDataStore = + webKitProxy.defaultWebsiteDataStore(); +} + +/// An implementation of [PlatformWebViewCookieManager] with the WebKit api. +class WebKitWebViewCookieManager extends PlatformWebViewCookieManager { + /// Constructs a [WebKitWebViewCookieManager]. + WebKitWebViewCookieManager(PlatformWebViewCookieManagerCreationParams params) + : super.implementation( + params is WebKitWebViewCookieManagerCreationParams + ? params + : WebKitWebViewCookieManagerCreationParams + .fromPlatformWebViewCookieManagerCreationParams(params), + ); + + WebKitWebViewCookieManagerCreationParams get _webkitParams => + params as WebKitWebViewCookieManagerCreationParams; + + @override + Future clearCookies() { + return _webkitParams._websiteDataStore.removeDataOfTypes( + {WKWebsiteDataType.cookies}, + DateTime.fromMillisecondsSinceEpoch(0), + ); + } + + @override + Future setCookie(WebViewCookie cookie) { + if (!_isValidPath(cookie.path)) { + throw ArgumentError( + 'The path property for the provided cookie was not given a legal value.', + ); + } + + return _webkitParams._websiteDataStore.httpCookieStore.setCookie( + NSHttpCookie.withProperties( + { + NSHttpCookiePropertyKey.name: cookie.name, + NSHttpCookiePropertyKey.value: cookie.value, + NSHttpCookiePropertyKey.domain: cookie.domain, + NSHttpCookiePropertyKey.path: cookie.path, + }, + ), + ); + } + + bool _isValidPath(String path) { + // Permitted ranges based on RFC6265bis: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 + return !path.codeUnits.any( + (int char) { + return (char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E); + }, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_platform.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_platform.dart new file mode 100644 index 000000000000..13116bb30b5c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_platform.dart @@ -0,0 +1,39 @@ +// 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:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; + +import 'webkit_webview_controller.dart'; +import 'webkit_webview_cookie_manager.dart'; + +/// Implementation of [WebViewPlatform] using the WebKit API. +class WebKitWebViewPlatform extends WebViewPlatform { + @override + WebKitWebViewController createPlatformWebViewController( + PlatformWebViewControllerCreationParams params, + ) { + return WebKitWebViewController(params); + } + + @override + WebKitNavigationDelegate createPlatformNavigationDelegate( + PlatformNavigationDelegateCreationParams params, + ) { + return WebKitNavigationDelegate(params); + } + + @override + WebKitWebViewWidget createPlatformWebViewWidget( + PlatformWebViewWidgetCreationParams params, + ) { + return WebKitWebViewWidget(params); + } + + @override + WebKitWebViewCookieManager createPlatformCookieManager( + PlatformWebViewCookieManagerCreationParams params, + ) { + return WebKitWebViewCookieManager(params); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/webview_flutter_wkwebview.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/webview_flutter_wkwebview.dart new file mode 100644 index 000000000000..f54fb73bcda3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/webview_flutter_wkwebview.dart @@ -0,0 +1,9 @@ +// 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 webview_flutter_wkwebview; + +export 'src/webkit_webview_controller.dart'; +export 'src/webkit_webview_cookie_manager.dart'; +export 'src/webkit_webview_platform.dart'; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart index d71509597f3f..566c46fda8bb 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart @@ -3,8 +3,12 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import '../common/instance_manager.dart'; import '../foundation/foundation.dart'; +import '../ui_kit/ui_kit.dart'; +import 'web_kit_api_impls.dart'; /// Times at which to inject script content into a webpage. /// @@ -46,6 +50,94 @@ enum WKAudiovisualMediaType { all, } +/// Types of data that websites store. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebsitedatarecord/data_store_record_types?language=objc. +enum WKWebsiteDataType { + /// Cookies. + cookies, + + /// In-memory caches. + memoryCache, + + /// On-disk caches. + diskCache, + + /// HTML offline web app caches. + offlineWebApplicationCache, + + /// HTML local storage. + localStorage, + + /// HTML session storage. + sessionStorage, + + /// WebSQL databases. + webSQLDatabases, + + /// IndexedDB databases. + indexedDBDatabases, +} + +/// Indicate whether to allow or cancel navigation to a webpage. +/// +/// Wraps [WKNavigationActionPolicy](https://developer.apple.com/documentation/webkit/wknavigationactionpolicy?language=objc). +enum WKNavigationActionPolicy { + /// Allow navigation to continue. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationactionpolicy/wknavigationactionpolicyallow?language=objc. + allow, + + /// Cancel navigation. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationactionpolicy/wknavigationactionpolicycancel?language=objc. + cancel, +} + +/// Possible error values that WebKit APIs can return. +/// +/// See https://developer.apple.com/documentation/webkit/wkerrorcode. +class WKErrorCode { + WKErrorCode._(); + + /// Indicates an unknown issue occurred. + /// + /// See https://developer.apple.com/documentation/webkit/wkerrorcode/wkerrorunknown. + static const int unknown = 1; + + /// Indicates the web process that contains the content is no longer running. + /// + /// See https://developer.apple.com/documentation/webkit/wkerrorcode/wkerrorwebcontentprocessterminated. + static const int webContentProcessTerminated = 2; + + /// Indicates the web view was invalidated. + /// + /// See https://developer.apple.com/documentation/webkit/wkerrorcode/wkerrorwebviewinvalidated. + static const int webViewInvalidated = 3; + + /// Indicates a JavaScript exception occurred. + /// + /// See https://developer.apple.com/documentation/webkit/wkerrorcode/wkerrorjavascriptexceptionoccurred. + static const int javaScriptExceptionOccurred = 4; + + /// Indicates the result of JavaScript execution could not be returned. + /// + /// See https://developer.apple.com/documentation/webkit/wkerrorcode/wkerrorjavascriptresulttypeisunsupported. + static const int javaScriptResultTypeIsUnsupported = 5; +} + +/// A record of the data that a particular website stores persistently. +/// +/// Wraps [WKWebsiteDataRecord](https://developer.apple.com/documentation/webkit/wkwebsitedatarecord?language=objc). +@immutable +class WKWebsiteDataRecord { + /// Constructs a [WKWebsiteDataRecord]. + const WKWebsiteDataRecord({required this.displayName}); + + /// Identifying information that you display to users. + final String displayName; +} + /// An object that contains information about an action that causes navigation to occur. /// /// Wraps [WKNavigationAction](https://developer.apple.com/documentation/webkit/wknavigationaction?language=objc). @@ -115,23 +207,256 @@ class WKScriptMessage { final Object? body; } +/// Encapsulates the standard behaviors to apply to websites. +/// +/// Wraps [WKPreferences](https://developer.apple.com/documentation/webkit/wkpreferences?language=objc). +@immutable +class WKPreferences extends NSObject { + /// Constructs a [WKPreferences] that is owned by [configuration]. + factory WKPreferences.fromWebViewConfiguration( + WKWebViewConfiguration configuration, { + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) { + final WKPreferences preferences = WKPreferences.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + preferences._preferencesApi.createFromWebViewConfigurationForInstances( + preferences, + configuration, + ); + return preferences; + } + + /// Constructs a [WKPreferences] without creating the associated + /// Objective-C object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + WKPreferences.detached({ + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _preferencesApi = WKPreferencesHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + final WKPreferencesHostApiImpl _preferencesApi; + + // TODO(bparrishMines): Deprecated for iOS 14.0+. Add support for alternative. + /// Sets whether JavaScript is enabled. + /// + /// The default value is true. + Future setJavaScriptEnabled(bool enabled) { + return _preferencesApi.setJavaScriptEnabledForInstances(this, enabled); + } + + @override + WKPreferences copy() { + return WKPreferences.detached( + observeValue: observeValue, + binaryMessenger: _preferencesApi.binaryMessenger, + instanceManager: _preferencesApi.instanceManager, + ); + } +} + +/// Manages cookies, disk and memory caches, and other types of data for a web view. +/// +/// Wraps [WKWebsiteDataStore](https://developer.apple.com/documentation/webkit/wkwebsitedatastore?language=objc). +@immutable +class WKWebsiteDataStore extends NSObject { + /// Constructs a [WKWebsiteDataStore] without creating the associated + /// Objective-C object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + WKWebsiteDataStore.detached({ + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _websiteDataStoreApi = WKWebsiteDataStoreHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + factory WKWebsiteDataStore._defaultDataStore() { + final WKWebsiteDataStore websiteDataStore = WKWebsiteDataStore.detached(); + websiteDataStore._websiteDataStoreApi.createDefaultDataStoreForInstances( + websiteDataStore, + ); + return websiteDataStore; + } + + /// Constructs a [WKWebsiteDataStore] that is owned by [configuration]. + factory WKWebsiteDataStore.fromWebViewConfiguration( + WKWebViewConfiguration configuration, { + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) { + final WKWebsiteDataStore websiteDataStore = WKWebsiteDataStore.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + websiteDataStore._websiteDataStoreApi + .createFromWebViewConfigurationForInstances( + websiteDataStore, + configuration, + ); + return websiteDataStore; + } + + /// Default data store that stores data persistently to disk. + static final WKWebsiteDataStore defaultDataStore = + WKWebsiteDataStore._defaultDataStore(); + + final WKWebsiteDataStoreHostApiImpl _websiteDataStoreApi; + + /// Manages the HTTP cookies associated with a particular web view. + late final WKHttpCookieStore httpCookieStore = + WKHttpCookieStore.fromWebsiteDataStore(this); + + /// Removes website data that changed after the specified date. + /// + /// Returns whether any data was removed. + Future removeDataOfTypes( + Set dataTypes, + DateTime since, + ) { + return _websiteDataStoreApi.removeDataOfTypesForInstances( + this, + dataTypes, + secondsModifiedSinceEpoch: since.millisecondsSinceEpoch / 1000, + ); + } + + @override + WKWebsiteDataStore copy() { + return WKWebsiteDataStore.detached( + observeValue: observeValue, + binaryMessenger: _websiteDataStoreApi.binaryMessenger, + instanceManager: _websiteDataStoreApi.instanceManager, + ); + } +} + +/// An object that manages the HTTP cookies associated with a particular web view. +/// +/// Wraps [WKHTTPCookieStore](https://developer.apple.com/documentation/webkit/wkhttpcookiestore?language=objc). +@immutable +class WKHttpCookieStore extends NSObject { + /// Constructs a [WKHttpCookieStore] without creating the associated + /// Objective-C object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + WKHttpCookieStore.detached({ + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _httpCookieStoreApi = WKHttpCookieStoreHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + /// Constructs a [WKHttpCookieStore] that is owned by [dataStore]. + factory WKHttpCookieStore.fromWebsiteDataStore( + WKWebsiteDataStore dataStore, { + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) { + final WKHttpCookieStore cookieStore = WKHttpCookieStore.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + cookieStore._httpCookieStoreApi.createFromWebsiteDataStoreForInstances( + cookieStore, + dataStore, + ); + return cookieStore; + } + + final WKHttpCookieStoreHostApiImpl _httpCookieStoreApi; + + /// Adds a cookie to the cookie store. + Future setCookie(NSHttpCookie cookie) { + return _httpCookieStoreApi.setCookieForInstances(this, cookie); + } + + @override + WKHttpCookieStore copy() { + return WKHttpCookieStore.detached( + observeValue: observeValue, + binaryMessenger: _httpCookieStoreApi.binaryMessenger, + instanceManager: _httpCookieStoreApi.instanceManager, + ); + } +} + /// An interface for receiving messages from JavaScript code running in a webpage. /// -/// Wraps [WKScriptMessageHandler](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler?language=objc) -class WKScriptMessageHandler { +/// Wraps [WKScriptMessageHandler](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler?language=objc). +@immutable +class WKScriptMessageHandler extends NSObject { + /// Constructs a [WKScriptMessageHandler]. + WKScriptMessageHandler({ + required this.didReceiveScriptMessage, + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _scriptMessageHandlerApi = WKScriptMessageHandlerHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached() { + // Ensures FlutterApis for the WebKit library are set up. + WebKitFlutterApis.instance.ensureSetUp(); + _scriptMessageHandlerApi.createForInstances(this); + } + + /// Constructs a [WKScriptMessageHandler] without creating the associated + /// Objective-C object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + WKScriptMessageHandler.detached({ + required this.didReceiveScriptMessage, + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _scriptMessageHandlerApi = WKScriptMessageHandlerHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + final WKScriptMessageHandlerHostApiImpl _scriptMessageHandlerApi; + /// Tells the handler that a webpage sent a script message. /// /// Use this method to respond to a message sent from the webpage’s /// JavaScript code. Use the [message] parameter to get the message contents and /// to determine the originating web view. - set didReceiveScriptMessage( - void Function( - WKUserContentController userContentController, - WKScriptMessage message, - ) - didReceiveScriptMessage, - ) { - throw UnimplementedError(); + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} + final void Function( + WKUserContentController userContentController, + WKScriptMessage message, + ) didReceiveScriptMessage; + + @override + WKScriptMessageHandler copy() { + return WKScriptMessageHandler.detached( + didReceiveScriptMessage: didReceiveScriptMessage, + observeValue: observeValue, + binaryMessenger: _scriptMessageHandlerApi.binaryMessenger, + instanceManager: _scriptMessageHandlerApi.instanceManager, + ); } } @@ -144,16 +469,43 @@ class WKScriptMessageHandler { /// code. /// /// Wraps [WKUserContentController](https://developer.apple.com/documentation/webkit/wkusercontentcontroller?language=objc). -class WKUserContentController { - /// Constructs a [WKUserContentController]. - WKUserContentController(); - - // A WKUserContentController that is owned by configuration. - WKUserContentController._fromWebViewConfiguretion( - // TODO(bparrishMines): Remove ignore once constructor is implemented. - // ignore: avoid_unused_constructor_parameters - WKWebViewConfiguration configuration, - ); +@immutable +class WKUserContentController extends NSObject { + /// Constructs a [WKUserContentController] that is owned by [configuration]. + factory WKUserContentController.fromWebViewConfiguration( + WKWebViewConfiguration configuration, { + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) { + final WKUserContentController userContentController = + WKUserContentController.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + userContentController._userContentControllerApi + .createFromWebViewConfigurationForInstances( + userContentController, + configuration, + ); + return userContentController; + } + + /// Constructs a [WKUserContentController] without creating the associated + /// Objective-C object. + /// + /// This should only be used outside of tests by subclasses created by this + /// library or to create a copy for an InstanceManager. + WKUserContentController.detached({ + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _userContentControllerApi = WKUserContentControllerHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + final WKUserContentControllerHostApiImpl _userContentControllerApi; /// Installs a message handler that you can call from your JavaScript code. /// @@ -171,7 +523,11 @@ class WKUserContentController { String name, ) { assert(name.isNotEmpty); - throw UnimplementedError(); + return _userContentControllerApi.addScriptMessageHandlerForInstances( + this, + handler, + name, + ); } /// Uninstalls the custom message handler with the specified name from your JavaScript code. @@ -184,80 +540,329 @@ class WKUserContentController { /// message handler from the page content world. If you installed the message /// handler in a different content world, this method doesn’t remove it. Future removeScriptMessageHandler(String name) { - throw UnimplementedError(); + return _userContentControllerApi.removeScriptMessageHandlerForInstances( + this, + name, + ); } - /// Uninstalls all custom message handlers associated with the user content controller. + /// Uninstalls all custom message handlers associated with the user content + /// controller. + /// + /// Only supported on iOS version 14+. Future removeAllScriptMessageHandlers() { - throw UnimplementedError(); + return _userContentControllerApi.removeAllScriptMessageHandlersForInstances( + this, + ); } /// Injects the specified script into the webpage’s content. Future addUserScript(WKUserScript userScript) { - throw UnimplementedError(); + return _userContentControllerApi.addUserScriptForInstances( + this, userScript); } /// Removes all user scripts from the web view. Future removeAllUserScripts() { - throw UnimplementedError(); + return _userContentControllerApi.removeAllUserScriptsForInstances(this); + } + + @override + WKUserContentController copy() { + return WKUserContentController.detached( + observeValue: observeValue, + binaryMessenger: _userContentControllerApi.binaryMessenger, + instanceManager: _userContentControllerApi.instanceManager, + ); } } /// A collection of properties that you use to initialize a web view. /// /// Wraps [WKWebViewConfiguration](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration?language=objc). -class WKWebViewConfiguration { +@immutable +class WKWebViewConfiguration extends NSObject { /// Constructs a [WKWebViewConfiguration]. - WKWebViewConfiguration({required this.userContentController}); + WKWebViewConfiguration({ + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _webViewConfigurationApi = WKWebViewConfigurationHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached() { + // Ensures FlutterApis for the WebKit library are set up. + WebKitFlutterApis.instance.ensureSetUp(); + _webViewConfigurationApi.createForInstances(this); + } - // A WKWebViewConfiguration that is owned by webView. - // TODO(bparrishMines): Remove ignore once constructor is implemented. - // ignore: avoid_unused_constructor_parameters - WKWebViewConfiguration._fromWebView(WKWebView webView) { - userContentController = - WKUserContentController._fromWebViewConfiguretion(this); + /// A WKWebViewConfiguration that is owned by webView. + @visibleForTesting + factory WKWebViewConfiguration.fromWebView( + WKWebView webView, { + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) { + final WKWebViewConfiguration configuration = + WKWebViewConfiguration.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + configuration._webViewConfigurationApi.createFromWebViewForInstances( + configuration, + webView, + ); + return configuration; } + /// Constructs a [WKWebViewConfiguration] without creating the associated + /// Objective-C object. + /// + /// This should only be used outside of tests by subclasses created by this + /// library or to create a copy for an InstanceManager. + WKWebViewConfiguration.detached({ + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _webViewConfigurationApi = WKWebViewConfigurationHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + late final WKWebViewConfigurationHostApiImpl _webViewConfigurationApi; + /// Coordinates interactions between your app’s code and the webpage’s scripts and other content. - late final WKUserContentController userContentController; + late final WKUserContentController userContentController = + WKUserContentController.fromWebViewConfiguration( + this, + binaryMessenger: _webViewConfigurationApi.binaryMessenger, + instanceManager: _webViewConfigurationApi.instanceManager, + ); + + /// Manages the preference-related settings for the web view. + late final WKPreferences preferences = WKPreferences.fromWebViewConfiguration( + this, + binaryMessenger: _webViewConfigurationApi.binaryMessenger, + instanceManager: _webViewConfigurationApi.instanceManager, + ); + + /// Used to get and set the site’s cookies and to track the cached data objects. + /// + /// Represents [WKWebViewConfiguration.webSiteDataStore](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/1395661-websitedatastore?language=objc). + late final WKWebsiteDataStore websiteDataStore = + WKWebsiteDataStore.fromWebViewConfiguration( + this, + binaryMessenger: _webViewConfigurationApi.binaryMessenger, + instanceManager: _webViewConfigurationApi.instanceManager, + ); /// Indicates whether HTML5 videos play inline or use the native full-screen controller. - set allowsInlineMediaPlayback(bool allow) { - throw UnimplementedError(); + /// + /// Sets [WKWebViewConfiguration.allowsInlineMediaPlayback](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/1614793-allowsinlinemediaplayback?language=objc). + Future setAllowsInlineMediaPlayback(bool allow) { + return _webViewConfigurationApi.setAllowsInlineMediaPlaybackForInstances( + this, + allow, + ); } /// The media types that require a user gesture to begin playing. /// /// Use [WKAudiovisualMediaType.none] to indicate that no user gestures are /// required to begin playing media. - set mediaTypesRequiringUserActionForPlayback( + /// + /// Sets [WKWebViewConfiguration.mediaTypesRequiringUserActionForPlayback](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/1851524-mediatypesrequiringuseractionfor?language=objc). + Future setMediaTypesRequiringUserActionForPlayback( Set types, ) { assert(types.isNotEmpty); - throw UnimplementedError(); + return _webViewConfigurationApi + .setMediaTypesRequiringUserActionForPlaybackForInstances( + this, + types, + ); + } + + @override + WKWebViewConfiguration copy() { + return WKWebViewConfiguration.detached( + observeValue: observeValue, + binaryMessenger: _webViewConfigurationApi.binaryMessenger, + instanceManager: _webViewConfigurationApi.instanceManager, + ); } } /// The methods for presenting native user interface elements on behalf of a webpage. /// /// Wraps [WKUIDelegate](https://developer.apple.com/documentation/webkit/wkuidelegate?language=objc). -class WKUIDelegate { - /// Indicates a new [WebView] was requested to be created with [configuration]. - set onCreateWebView( - void Function( - WKWebViewConfiguration configuration, - WKNavigationAction navigationAction, - ) - onCreateeWebView, - ) { - throw UnimplementedError(); +@immutable +class WKUIDelegate extends NSObject { + /// Constructs a [WKUIDelegate]. + WKUIDelegate({ + this.onCreateWebView, + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _uiDelegateApi = WKUIDelegateHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached() { + // Ensures FlutterApis for the WebKit library are set up. + WebKitFlutterApis.instance.ensureSetUp(); + _uiDelegateApi.createForInstances(this); + } + + /// Constructs a [WKUIDelegate] without creating the associated Objective-C + /// object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + WKUIDelegate.detached({ + this.onCreateWebView, + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _uiDelegateApi = WKUIDelegateHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + final WKUIDelegateHostApiImpl _uiDelegateApi; + + /// Indicates a new [WKWebView] was requested to be created with [configuration]. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} + final void Function( + WKWebView webView, + WKWebViewConfiguration configuration, + WKNavigationAction navigationAction, + )? onCreateWebView; + + @override + WKUIDelegate copy() { + return WKUIDelegate.detached( + onCreateWebView: onCreateWebView, + observeValue: observeValue, + binaryMessenger: _uiDelegateApi.binaryMessenger, + instanceManager: _uiDelegateApi.instanceManager, + ); + } +} + +/// Methods for handling navigation changes and tracking navigation requests. +/// +/// Set the methods of the [WKNavigationDelegate] in the object you use to +/// coordinate changes in your web view’s main frame. +/// +/// Wraps [WKNavigationDelegate](https://developer.apple.com/documentation/webkit/wknavigationdelegate?language=objc). +@immutable +class WKNavigationDelegate extends NSObject { + /// Constructs a [WKNavigationDelegate]. + WKNavigationDelegate({ + this.didFinishNavigation, + this.didStartProvisionalNavigation, + this.decidePolicyForNavigationAction, + this.didFailNavigation, + this.didFailProvisionalNavigation, + this.webViewWebContentProcessDidTerminate, + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _navigationDelegateApi = WKNavigationDelegateHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached() { + // Ensures FlutterApis for the WebKit library are set up. + WebKitFlutterApis.instance.ensureSetUp(); + _navigationDelegateApi.createForInstances(this); + } + + /// Constructs a [WKNavigationDelegate] without creating the associated + /// Objective-C object. + /// + /// This should only be used outside of tests by subclasses created by this + /// library or to create a copy for an InstanceManager. + WKNavigationDelegate.detached({ + this.didFinishNavigation, + this.didStartProvisionalNavigation, + this.decidePolicyForNavigationAction, + this.didFailNavigation, + this.didFailProvisionalNavigation, + this.webViewWebContentProcessDidTerminate, + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _navigationDelegateApi = WKNavigationDelegateHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + final WKNavigationDelegateHostApiImpl _navigationDelegateApi; + + /// Called when navigation is complete. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} + final void Function(WKWebView webView, String? url)? didFinishNavigation; + + /// Called when navigation from the main frame has started. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} + final void Function(WKWebView webView, String? url)? + didStartProvisionalNavigation; + + /// Called when permission is needed to navigate to new content. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} + final Future Function( + WKWebView webView, + WKNavigationAction navigationAction, + )? decidePolicyForNavigationAction; + + /// Called when an error occurred during navigation. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} + final void Function(WKWebView webView, NSError error)? didFailNavigation; + + /// Called when an error occurred during the early navigation process. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} + final void Function(WKWebView webView, NSError error)? + didFailProvisionalNavigation; + + /// Called when the web view’s content process was terminated. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} + final void Function(WKWebView webView)? webViewWebContentProcessDidTerminate; + + @override + WKNavigationDelegate copy() { + return WKNavigationDelegate.detached( + didFinishNavigation: didFinishNavigation, + didStartProvisionalNavigation: didStartProvisionalNavigation, + decidePolicyForNavigationAction: decidePolicyForNavigationAction, + didFailNavigation: didFailNavigation, + didFailProvisionalNavigation: didFailProvisionalNavigation, + webViewWebContentProcessDidTerminate: + webViewWebContentProcessDidTerminate, + observeValue: observeValue, + binaryMessenger: _navigationDelegateApi.binaryMessenger, + instanceManager: _navigationDelegateApi.instanceManager, + ); } } /// Object that displays interactive web content, such as for an in-app browser. /// /// Wraps [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview?language=objc). -class WKWebView { +@immutable +class WKWebView extends UIView { /// Constructs a [WKWebView]. /// /// [configuration] contains the configuration details for the web view. This @@ -267,12 +872,38 @@ class WKWebView { /// values, see [WKWebViewConfiguration]. If you didn’t create your web view /// using the `configuration` parameter, this value uses a default /// configuration object. - // TODO(bparrishMines): Remove ignore once constructor is implemented. - // ignore: avoid_unused_constructor_parameters - WKWebView([WKWebViewConfiguration? configuration]) { - throw UnimplementedError(); + WKWebView( + WKWebViewConfiguration configuration, { + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _webViewApi = WKWebViewHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached() { + // Ensures FlutterApis for the WebKit library are set up. + WebKitFlutterApis.instance.ensureSetUp(); + _webViewApi.createForInstances(this, configuration); } + /// Constructs a [WKWebView] without creating the associated Objective-C + /// object. + /// + /// This should only be used outside of tests by subclasses created by this + /// library or to create a copy for an InstanceManager. + WKWebView.detached({ + super.observeValue, + super.binaryMessenger, + super.instanceManager, + }) : _webViewApi = WKWebViewHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + final WKWebViewHostApiImpl _webViewApi; + /// Contains the configuration details for the web view. /// /// Use the object in this property to obtain information about your web @@ -283,11 +914,47 @@ class WKWebView { /// If you didn’t create your web view with a [WKWebViewConfiguration] this /// property contains a default configuration object. late final WKWebViewConfiguration configuration = - WKWebViewConfiguration._fromWebView(this); + WKWebViewConfiguration.fromWebView( + this, + binaryMessenger: _webViewApi.binaryMessenger, + instanceManager: _webViewApi.instanceManager, + ); + + /// The scrollable view associated with the web view. + late final UIScrollView scrollView = UIScrollView.fromWebView( + this, + binaryMessenger: _webViewApi.binaryMessenger, + instanceManager: _webViewApi.instanceManager, + ); /// Used to integrate custom user interface elements into web view interactions. - set uiDelegate(WKUIDelegate delegate) { - throw UnimplementedError(); + /// + /// Sets [WKWebView.UIDelegate](https://developer.apple.com/documentation/webkit/wkwebview/1415009-uidelegate?language=objc). + Future setUIDelegate(WKUIDelegate? delegate) { + return _webViewApi.setUIDelegateForInstances(this, delegate); + } + + /// The object you use to manage navigation behavior for the web view. + /// + /// Sets [WKWebView.navigationDelegate](https://developer.apple.com/documentation/webkit/wkwebview/1414971-navigationdelegate?language=objc). + Future setNavigationDelegate(WKNavigationDelegate? delegate) { + return _webViewApi.setNavigationDelegateForInstances(this, delegate); + } + + /// The URL for the current webpage. + /// + /// Represents [WKWebView.URL](https://developer.apple.com/documentation/webkit/wkwebview/1415005-url?language=objc). + Future getUrl() { + return _webViewApi.getUrlForInstances(this); + } + + /// An estimate of what fraction of the current navigation has been loaded. + /// + /// This value ranges from 0.0 to 1.0. + /// + /// Represents [WKWebView.estimatedProgress](https://developer.apple.com/documentation/webkit/wkwebview/1415007-estimatedprogress?language=objc). + Future getEstimatedProgress() { + return _webViewApi.getEstimatedProgressForInstances(this); } /// Loads the web content referenced by the specified URL request object and navigates to it. @@ -295,6 +962,97 @@ class WKWebView { /// Use this method to load a page from a local or network-based URL. For /// example, you might use it to navigate to a network-based webpage. Future loadRequest(NSUrlRequest request) { - throw UnimplementedError(); + return _webViewApi.loadRequestForInstances(this, request); + } + + /// Loads the contents of the specified HTML string and navigates to it. + Future loadHtmlString(String string, {String? baseUrl}) { + return _webViewApi.loadHtmlStringForInstances(this, string, baseUrl); + } + + /// Loads the web content from the specified file and navigates to it. + Future loadFileUrl(String url, {required String readAccessUrl}) { + return _webViewApi.loadFileUrlForInstances(this, url, readAccessUrl); + } + + /// Loads the Flutter asset specified in the pubspec.yaml file. + /// + /// This method is not a part of WebKit and is only a Flutter specific helper + /// method. + Future loadFlutterAsset(String key) { + return _webViewApi.loadFlutterAssetForInstances(this, key); + } + + /// Indicates whether there is a valid back item in the back-forward list. + Future canGoBack() { + return _webViewApi.canGoBackForInstances(this); + } + + /// Indicates whether there is a valid forward item in the back-forward list. + Future canGoForward() { + return _webViewApi.canGoForwardForInstances(this); + } + + /// Navigates to the back item in the back-forward list. + Future goBack() { + return _webViewApi.goBackForInstances(this); + } + + /// Navigates to the forward item in the back-forward list. + Future goForward() { + return _webViewApi.goForwardForInstances(this); + } + + /// Reloads the current webpage. + Future reload() { + return _webViewApi.reloadForInstances(this); + } + + /// The page title. + /// + /// Represents [WKWebView.title](https://developer.apple.com/documentation/webkit/wkwebview/1415015-title?language=objc). + Future getTitle() { + return _webViewApi.getTitleForInstances(this); + } + + /// Indicates whether horizontal swipe gestures trigger page navigation. + /// + /// The default value is false. + /// + /// Sets [WKWebView.allowsBackForwardNavigationGestures](https://developer.apple.com/documentation/webkit/wkwebview/1414995-allowsbackforwardnavigationgestu?language=objc). + Future setAllowsBackForwardNavigationGestures(bool allow) { + return _webViewApi.setAllowsBackForwardNavigationGesturesForInstances( + this, + allow, + ); + } + + /// The custom user agent string. + /// + /// The default value of this property is null. + /// + /// Sets [WKWebView.customUserAgent](https://developer.apple.com/documentation/webkit/wkwebview/1414950-customuseragent?language=objc). + Future setCustomUserAgent(String? userAgent) { + return _webViewApi.setCustomUserAgentForInstances(this, userAgent); + } + + /// Evaluates the specified JavaScript string. + /// + /// Throws a `PlatformException` if an error occurs or return value is not + /// supported. + Future evaluateJavaScript(String javaScriptString) { + return _webViewApi.evaluateJavaScriptForInstances( + this, + javaScriptString, + ); + } + + @override + WKWebView copy() { + return WKWebView.detached( + observeValue: observeValue, + binaryMessenger: _webViewApi.binaryMessenger, + instanceManager: _webViewApi.instanceManager, + ); } } 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 new file mode 100644 index 000000000000..614d0793e5f9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart @@ -0,0 +1,1040 @@ +// 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/services.dart'; + +import '../common/instance_manager.dart'; +import '../common/web_kit.pigeon.dart'; +import '../foundation/foundation.dart'; +import 'web_kit.dart'; + +Iterable _toWKWebsiteDataTypeEnumData( + Iterable types) { + return types.map((WKWebsiteDataType type) { + late final WKWebsiteDataTypeEnum value; + switch (type) { + case WKWebsiteDataType.cookies: + value = WKWebsiteDataTypeEnum.cookies; + break; + case WKWebsiteDataType.memoryCache: + value = WKWebsiteDataTypeEnum.memoryCache; + break; + case WKWebsiteDataType.diskCache: + value = WKWebsiteDataTypeEnum.diskCache; + break; + case WKWebsiteDataType.offlineWebApplicationCache: + value = WKWebsiteDataTypeEnum.offlineWebApplicationCache; + break; + case WKWebsiteDataType.localStorage: + value = WKWebsiteDataTypeEnum.localStorage; + break; + case WKWebsiteDataType.sessionStorage: + value = WKWebsiteDataTypeEnum.sessionStorage; + break; + case WKWebsiteDataType.webSQLDatabases: + value = WKWebsiteDataTypeEnum.webSQLDatabases; + break; + case WKWebsiteDataType.indexedDBDatabases: + value = WKWebsiteDataTypeEnum.indexedDBDatabases; + break; + } + + return WKWebsiteDataTypeEnumData(value: value); + }); +} + +extension _NSHttpCookieConverter on NSHttpCookie { + NSHttpCookieData toNSHttpCookieData() { + final Iterable keys = properties.keys; + return NSHttpCookieData( + propertyKeys: keys.map( + (NSHttpCookiePropertyKey key) { + return key.toNSHttpCookiePropertyKeyEnumData(); + }, + ).toList(), + propertyValues: keys + .map((NSHttpCookiePropertyKey key) => properties[key]!) + .toList(), + ); + } +} + +extension _WKNavigationActionPolicyConverter on WKNavigationActionPolicy { + WKNavigationActionPolicyEnumData toWKNavigationActionPolicyEnumData() { + return WKNavigationActionPolicyEnumData( + value: WKNavigationActionPolicyEnum.values.firstWhere( + (WKNavigationActionPolicyEnum element) => element.name == name, + ), + ); + } +} + +extension _NSHttpCookiePropertyKeyConverter on NSHttpCookiePropertyKey { + NSHttpCookiePropertyKeyEnumData toNSHttpCookiePropertyKeyEnumData() { + late final NSHttpCookiePropertyKeyEnum value; + switch (this) { + case NSHttpCookiePropertyKey.comment: + value = NSHttpCookiePropertyKeyEnum.comment; + break; + case NSHttpCookiePropertyKey.commentUrl: + value = NSHttpCookiePropertyKeyEnum.commentUrl; + break; + case NSHttpCookiePropertyKey.discard: + value = NSHttpCookiePropertyKeyEnum.discard; + break; + case NSHttpCookiePropertyKey.domain: + value = NSHttpCookiePropertyKeyEnum.domain; + break; + case NSHttpCookiePropertyKey.expires: + value = NSHttpCookiePropertyKeyEnum.expires; + break; + case NSHttpCookiePropertyKey.maximumAge: + value = NSHttpCookiePropertyKeyEnum.maximumAge; + break; + case NSHttpCookiePropertyKey.name: + value = NSHttpCookiePropertyKeyEnum.name; + break; + case NSHttpCookiePropertyKey.originUrl: + value = NSHttpCookiePropertyKeyEnum.originUrl; + break; + case NSHttpCookiePropertyKey.path: + value = NSHttpCookiePropertyKeyEnum.path; + break; + case NSHttpCookiePropertyKey.port: + value = NSHttpCookiePropertyKeyEnum.port; + break; + case NSHttpCookiePropertyKey.sameSitePolicy: + value = NSHttpCookiePropertyKeyEnum.sameSitePolicy; + break; + case NSHttpCookiePropertyKey.secure: + value = NSHttpCookiePropertyKeyEnum.secure; + break; + case NSHttpCookiePropertyKey.value: + value = NSHttpCookiePropertyKeyEnum.value; + break; + case NSHttpCookiePropertyKey.version: + value = NSHttpCookiePropertyKeyEnum.version; + break; + } + + return NSHttpCookiePropertyKeyEnumData(value: value); + } +} + +extension _WKUserScriptInjectionTimeConverter on WKUserScriptInjectionTime { + WKUserScriptInjectionTimeEnumData toWKUserScriptInjectionTimeEnumData() { + late final WKUserScriptInjectionTimeEnum value; + switch (this) { + case WKUserScriptInjectionTime.atDocumentStart: + value = WKUserScriptInjectionTimeEnum.atDocumentStart; + break; + case WKUserScriptInjectionTime.atDocumentEnd: + value = WKUserScriptInjectionTimeEnum.atDocumentEnd; + break; + } + + return WKUserScriptInjectionTimeEnumData(value: value); + } +} + +Iterable _toWKAudiovisualMediaTypeEnumData( + Iterable types, +) { + return types + .map((WKAudiovisualMediaType type) { + late final WKAudiovisualMediaTypeEnum value; + switch (type) { + case WKAudiovisualMediaType.none: + value = WKAudiovisualMediaTypeEnum.none; + break; + case WKAudiovisualMediaType.audio: + value = WKAudiovisualMediaTypeEnum.audio; + break; + case WKAudiovisualMediaType.video: + value = WKAudiovisualMediaTypeEnum.video; + break; + case WKAudiovisualMediaType.all: + value = WKAudiovisualMediaTypeEnum.all; + break; + } + + return WKAudiovisualMediaTypeEnumData(value: value); + }); +} + +extension _NavigationActionDataConverter on WKNavigationActionData { + WKNavigationAction toNavigationAction() { + return WKNavigationAction( + request: request.toNSUrlRequest(), + targetFrame: targetFrame.toWKFrameInfo(), + ); + } +} + +extension _WKFrameInfoDataConverter on WKFrameInfoData { + WKFrameInfo toWKFrameInfo() { + return WKFrameInfo(isMainFrame: isMainFrame); + } +} + +extension _NSUrlRequestDataConverter on NSUrlRequestData { + NSUrlRequest toNSUrlRequest() { + return NSUrlRequest( + url: url, + httpBody: httpBody, + httpMethod: httpMethod, + allHttpHeaderFields: allHttpHeaderFields.cast(), + ); + } +} + +extension _WKNSErrorDataConverter on NSErrorData { + NSError toNSError() { + return NSError( + domain: domain, + code: code, + localizedDescription: localizedDescription, + ); + } +} + +extension _WKScriptMessageDataConverter on WKScriptMessageData { + WKScriptMessage toWKScriptMessage() { + return WKScriptMessage(name: name, body: body); + } +} + +extension _WKUserScriptConverter on WKUserScript { + WKUserScriptData toWKUserScriptData() { + return WKUserScriptData( + source: source, + injectionTime: injectionTime.toWKUserScriptInjectionTimeEnumData(), + isMainFrameOnly: isMainFrameOnly, + ); + } +} + +extension _NSUrlRequestConverter on NSUrlRequest { + NSUrlRequestData toNSUrlRequestData() { + return NSUrlRequestData( + url: url, + httpMethod: httpMethod, + httpBody: httpBody, + allHttpHeaderFields: allHttpHeaderFields, + ); + } +} + +/// Handles initialization of Flutter APIs for WebKit. +class WebKitFlutterApis { + /// Constructs a [WebKitFlutterApis]. + @visibleForTesting + WebKitFlutterApis({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : _binaryMessenger = binaryMessenger, + navigationDelegate = WKNavigationDelegateFlutterApiImpl( + instanceManager: instanceManager, + ), + scriptMessageHandler = WKScriptMessageHandlerFlutterApiImpl( + instanceManager: instanceManager, + ), + uiDelegate = WKUIDelegateFlutterApiImpl( + instanceManager: instanceManager, + ), + webViewConfiguration = WKWebViewConfigurationFlutterApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + + static WebKitFlutterApis _instance = WebKitFlutterApis(); + + /// Sets the global instance containing the Flutter Apis for the WebKit library. + @visibleForTesting + static set instance(WebKitFlutterApis instance) { + _instance = instance; + } + + /// Global instance containing the Flutter Apis for the WebKit library. + static WebKitFlutterApis get instance { + return _instance; + } + + final BinaryMessenger? _binaryMessenger; + bool _hasBeenSetUp = false; + + /// Flutter Api for [WKNavigationDelegate]. + @visibleForTesting + final WKNavigationDelegateFlutterApiImpl navigationDelegate; + + /// Flutter Api for [WKScriptMessageHandler]. + @visibleForTesting + final WKScriptMessageHandlerFlutterApiImpl scriptMessageHandler; + + /// Flutter Api for [WKUIDelegate]. + @visibleForTesting + final WKUIDelegateFlutterApiImpl uiDelegate; + + /// Flutter Api for [WKWebViewConfiguration]. + @visibleForTesting + final WKWebViewConfigurationFlutterApiImpl webViewConfiguration; + + /// Ensures all the Flutter APIs have been set up to receive calls from native code. + void ensureSetUp() { + if (!_hasBeenSetUp) { + WKNavigationDelegateFlutterApi.setup( + navigationDelegate, + binaryMessenger: _binaryMessenger, + ); + WKScriptMessageHandlerFlutterApi.setup( + scriptMessageHandler, + binaryMessenger: _binaryMessenger, + ); + WKUIDelegateFlutterApi.setup( + uiDelegate, + binaryMessenger: _binaryMessenger, + ); + WKWebViewConfigurationFlutterApi.setup( + webViewConfiguration, + binaryMessenger: _binaryMessenger, + ); + _hasBeenSetUp = true; + } + } +} + +/// Host api implementation for [WKWebSiteDataStore]. +class WKWebsiteDataStoreHostApiImpl extends WKWebsiteDataStoreHostApi { + /// Constructs a [WebsiteDataStoreHostApiImpl]. + WKWebsiteDataStoreHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [createFromWebViewConfiguration] with the ids of the provided object instances. + Future createFromWebViewConfigurationForInstances( + WKWebsiteDataStore instance, + WKWebViewConfiguration configuration, + ) { + return createFromWebViewConfiguration( + instanceManager.addDartCreatedInstance(instance), + instanceManager.getIdentifier(configuration)!, + ); + } + + /// Calls [createDefaultDataStore] with the ids of the provided object instances. + Future createDefaultDataStoreForInstances( + WKWebsiteDataStore instance, + ) { + return createDefaultDataStore( + instanceManager.addDartCreatedInstance(instance), + ); + } + + /// Calls [removeDataOfTypes] with the ids of the provided object instances. + Future removeDataOfTypesForInstances( + WKWebsiteDataStore instance, + Set dataTypes, { + required double secondsModifiedSinceEpoch, + }) { + return removeDataOfTypes( + instanceManager.getIdentifier(instance)!, + _toWKWebsiteDataTypeEnumData(dataTypes).toList(), + secondsModifiedSinceEpoch, + ); + } +} + +/// Host api implementation for [WKScriptMessageHandler]. +class WKScriptMessageHandlerHostApiImpl extends WKScriptMessageHandlerHostApi { + /// Constructs a [WKScriptMessageHandlerHostApiImpl]. + WKScriptMessageHandlerHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [create] with the ids of the provided object instances. + Future createForInstances(WKScriptMessageHandler instance) { + return create(instanceManager.addDartCreatedInstance(instance)); + } +} + +/// Flutter api implementation for [WKScriptMessageHandler]. +class WKScriptMessageHandlerFlutterApiImpl + extends WKScriptMessageHandlerFlutterApi { + /// Constructs a [WKScriptMessageHandlerFlutterApiImpl]. + WKScriptMessageHandlerFlutterApiImpl({InstanceManager? instanceManager}) + : instanceManager = instanceManager ?? NSObject.globalInstanceManager; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + WKScriptMessageHandler _getHandler(int identifier) { + return instanceManager.getInstanceWithWeakReference(identifier)!; + } + + @override + void didReceiveScriptMessage( + int identifier, + int userContentControllerIdentifier, + WKScriptMessageData message, + ) { + _getHandler(identifier).didReceiveScriptMessage( + instanceManager.getInstanceWithWeakReference( + userContentControllerIdentifier, + )! as WKUserContentController, + message.toWKScriptMessage(), + ); + } +} + +/// Host api implementation for [WKPreferences]. +class WKPreferencesHostApiImpl extends WKPreferencesHostApi { + /// Constructs a [WKPreferencesHostApiImpl]. + WKPreferencesHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [createFromWebViewConfiguration] with the ids of the provided object instances. + Future createFromWebViewConfigurationForInstances( + WKPreferences instance, + WKWebViewConfiguration configuration, + ) { + return createFromWebViewConfiguration( + instanceManager.addDartCreatedInstance(instance), + instanceManager.getIdentifier(configuration)!, + ); + } + + /// Calls [setJavaScriptEnabled] with the ids of the provided object instances. + Future setJavaScriptEnabledForInstances( + WKPreferences instance, + bool enabled, + ) { + return setJavaScriptEnabled( + instanceManager.getIdentifier(instance)!, + enabled, + ); + } +} + +/// Host api implementation for [WKHttpCookieStore]. +class WKHttpCookieStoreHostApiImpl extends WKHttpCookieStoreHostApi { + /// Constructs a [WKHttpCookieStoreHostApiImpl]. + WKHttpCookieStoreHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [createFromWebsiteDataStore] with the ids of the provided object instances. + Future createFromWebsiteDataStoreForInstances( + WKHttpCookieStore instance, + WKWebsiteDataStore dataStore, + ) { + return createFromWebsiteDataStore( + instanceManager.addDartCreatedInstance(instance), + instanceManager.getIdentifier(dataStore)!, + ); + } + + /// Calls [setCookie] with the ids of the provided object instances. + Future setCookieForInstances( + WKHttpCookieStore instance, + NSHttpCookie cookie, + ) { + return setCookie( + instanceManager.getIdentifier(instance)!, + cookie.toNSHttpCookieData(), + ); + } +} + +/// Host api implementation for [WKUserContentController]. +class WKUserContentControllerHostApiImpl + extends WKUserContentControllerHostApi { + /// Constructs a [WKUserContentControllerHostApiImpl]. + WKUserContentControllerHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [createFromWebViewConfiguration] with the ids of the provided object instances. + Future createFromWebViewConfigurationForInstances( + WKUserContentController instance, + WKWebViewConfiguration configuration, + ) { + return createFromWebViewConfiguration( + instanceManager.addDartCreatedInstance(instance), + instanceManager.getIdentifier(configuration)!, + ); + } + + /// Calls [addScriptMessageHandler] with the ids of the provided object instances. + Future addScriptMessageHandlerForInstances( + WKUserContentController instance, + WKScriptMessageHandler handler, + String name, + ) { + return addScriptMessageHandler( + instanceManager.getIdentifier(instance)!, + instanceManager.getIdentifier(handler)!, + name, + ); + } + + /// Calls [removeScriptMessageHandler] with the ids of the provided object instances. + Future removeScriptMessageHandlerForInstances( + WKUserContentController instance, + String name, + ) { + return removeScriptMessageHandler( + instanceManager.getIdentifier(instance)!, + name, + ); + } + + /// Calls [removeAllScriptMessageHandlers] with the ids of the provided object instances. + Future removeAllScriptMessageHandlersForInstances( + WKUserContentController instance, + ) { + return removeAllScriptMessageHandlers( + instanceManager.getIdentifier(instance)!, + ); + } + + /// Calls [addUserScript] with the ids of the provided object instances. + Future addUserScriptForInstances( + WKUserContentController instance, + WKUserScript userScript, + ) { + return addUserScript( + instanceManager.getIdentifier(instance)!, + userScript.toWKUserScriptData(), + ); + } + + /// Calls [removeAllUserScripts] with the ids of the provided object instances. + Future removeAllUserScriptsForInstances( + WKUserContentController instance, + ) { + return removeAllUserScripts(instanceManager.getIdentifier(instance)!); + } +} + +/// Host api implementation for [WKWebViewConfiguration]. +class WKWebViewConfigurationHostApiImpl extends WKWebViewConfigurationHostApi { + /// Constructs a [WKWebViewConfigurationHostApiImpl]. + WKWebViewConfigurationHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [create] with the ids of the provided object instances. + Future createForInstances(WKWebViewConfiguration instance) { + return create(instanceManager.addDartCreatedInstance(instance)); + } + + /// Calls [createFromWebView] with the ids of the provided object instances. + Future createFromWebViewForInstances( + WKWebViewConfiguration instance, + WKWebView webView, + ) { + return createFromWebView( + instanceManager.addDartCreatedInstance(instance), + instanceManager.getIdentifier(webView)!, + ); + } + + /// Calls [setAllowsInlineMediaPlayback] with the ids of the provided object instances. + Future setAllowsInlineMediaPlaybackForInstances( + WKWebViewConfiguration instance, + bool allow, + ) { + return setAllowsInlineMediaPlayback( + instanceManager.getIdentifier(instance)!, + allow, + ); + } + + /// Calls [setMediaTypesRequiringUserActionForPlayback] with the ids of the provided object instances. + Future setMediaTypesRequiringUserActionForPlaybackForInstances( + WKWebViewConfiguration instance, + Set types, + ) { + return setMediaTypesRequiringUserActionForPlayback( + instanceManager.getIdentifier(instance)!, + _toWKAudiovisualMediaTypeEnumData(types).toList(), + ); + } +} + +/// Flutter api implementation for [WKWebViewConfiguration]. +@immutable +class WKWebViewConfigurationFlutterApiImpl + extends WKWebViewConfigurationFlutterApi { + /// Constructs a [WKWebViewConfigurationFlutterApiImpl]. + WKWebViewConfigurationFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.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( + WKWebViewConfiguration.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, + ); + } +} + +/// Host api implementation for [WKUIDelegate]. +class WKUIDelegateHostApiImpl extends WKUIDelegateHostApi { + /// Constructs a [WKUIDelegateHostApiImpl]. + WKUIDelegateHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [create] with the ids of the provided object instances. + Future createForInstances(WKUIDelegate instance) async { + return create(instanceManager.addDartCreatedInstance(instance)); + } +} + +/// Flutter api implementation for [WKUIDelegate]. +class WKUIDelegateFlutterApiImpl extends WKUIDelegateFlutterApi { + /// Constructs a [WKUIDelegateFlutterApiImpl]. + WKUIDelegateFlutterApiImpl({InstanceManager? instanceManager}) + : instanceManager = instanceManager ?? NSObject.globalInstanceManager; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + WKUIDelegate _getDelegate(int identifier) { + return instanceManager.getInstanceWithWeakReference(identifier)!; + } + + @override + void onCreateWebView( + int identifier, + int webViewIdentifier, + int configurationIdentifier, + WKNavigationActionData navigationAction, + ) { + final void Function(WKWebView, WKWebViewConfiguration, WKNavigationAction)? + function = _getDelegate(identifier).onCreateWebView; + function?.call( + instanceManager.getInstanceWithWeakReference(webViewIdentifier)! + as WKWebView, + instanceManager.getInstanceWithWeakReference(configurationIdentifier)! + as WKWebViewConfiguration, + navigationAction.toNavigationAction(), + ); + } +} + +/// Host api implementation for [WKNavigationDelegate]. +class WKNavigationDelegateHostApiImpl extends WKNavigationDelegateHostApi { + /// Constructs a [WKNavigationDelegateHostApiImpl]. + WKNavigationDelegateHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [create] with the ids of the provided object instances. + Future createForInstances(WKNavigationDelegate instance) async { + return create(instanceManager.addDartCreatedInstance(instance)); + } +} + +/// Flutter api implementation for [WKNavigationDelegate]. +class WKNavigationDelegateFlutterApiImpl + extends WKNavigationDelegateFlutterApi { + /// Constructs a [WKNavigationDelegateFlutterApiImpl]. + WKNavigationDelegateFlutterApiImpl({InstanceManager? instanceManager}) + : instanceManager = instanceManager ?? NSObject.globalInstanceManager; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + WKNavigationDelegate _getDelegate(int identifier) { + return instanceManager.getInstanceWithWeakReference(identifier)!; + } + + @override + void didFinishNavigation( + int identifier, + int webViewIdentifier, + String? url, + ) { + final void Function(WKWebView, String?)? function = + _getDelegate(identifier).didFinishNavigation; + function?.call( + instanceManager.getInstanceWithWeakReference(webViewIdentifier)! + as WKWebView, + url, + ); + } + + @override + Future decidePolicyForNavigationAction( + int identifier, + int webViewIdentifier, + WKNavigationActionData navigationAction, + ) async { + final Future Function( + WKWebView, + WKNavigationAction navigationAction, + )? function = _getDelegate(identifier).decidePolicyForNavigationAction; + + if (function == null) { + return WKNavigationActionPolicyEnumData( + value: WKNavigationActionPolicyEnum.allow, + ); + } + + final WKNavigationActionPolicy policy = await function( + instanceManager.getInstanceWithWeakReference(webViewIdentifier)! + as WKWebView, + navigationAction.toNavigationAction(), + ); + return policy.toWKNavigationActionPolicyEnumData(); + } + + @override + void didFailNavigation( + int identifier, + int webViewIdentifier, + NSErrorData error, + ) { + final void Function(WKWebView, NSError)? function = + _getDelegate(identifier).didFailNavigation; + function?.call( + instanceManager.getInstanceWithWeakReference(webViewIdentifier)! + as WKWebView, + error.toNSError(), + ); + } + + @override + void didFailProvisionalNavigation( + int identifier, + int webViewIdentifier, + NSErrorData error, + ) { + final void Function(WKWebView, NSError)? function = + _getDelegate(identifier).didFailProvisionalNavigation; + function?.call( + instanceManager.getInstanceWithWeakReference(webViewIdentifier)! + as WKWebView, + error.toNSError(), + ); + } + + @override + void didStartProvisionalNavigation( + int identifier, + int webViewIdentifier, + String? url, + ) { + final void Function(WKWebView, String?)? function = + _getDelegate(identifier).didStartProvisionalNavigation; + function?.call( + instanceManager.getInstanceWithWeakReference(webViewIdentifier)! + as WKWebView, + url, + ); + } + + @override + void webViewWebContentProcessDidTerminate( + int identifier, + int webViewIdentifier, + ) { + final void Function(WKWebView)? function = + _getDelegate(identifier).webViewWebContentProcessDidTerminate; + function?.call( + instanceManager.getInstanceWithWeakReference(webViewIdentifier)! + as WKWebView, + ); + } +} + +/// Host api implementation for [WKWebView]. +class WKWebViewHostApiImpl extends WKWebViewHostApi { + /// Constructs a [WKWebViewHostApiImpl]. + WKWebViewHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? NSObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with Objective-C objects. + final InstanceManager instanceManager; + + /// Calls [create] with the ids of the provided object instances. + Future createForInstances( + WKWebView instance, + WKWebViewConfiguration configuration, + ) { + return create( + instanceManager.addDartCreatedInstance(instance), + instanceManager.getIdentifier(configuration)!, + ); + } + + /// Calls [loadRequest] with the ids of the provided object instances. + Future loadRequestForInstances( + WKWebView webView, + NSUrlRequest request, + ) { + return loadRequest( + instanceManager.getIdentifier(webView)!, + request.toNSUrlRequestData(), + ); + } + + /// Calls [loadHtmlString] with the ids of the provided object instances. + Future loadHtmlStringForInstances( + WKWebView instance, + String string, + String? baseUrl, + ) { + return loadHtmlString( + instanceManager.getIdentifier(instance)!, + string, + baseUrl, + ); + } + + /// Calls [loadFileUrl] with the ids of the provided object instances. + Future loadFileUrlForInstances( + WKWebView instance, + String url, + String readAccessUrl, + ) { + return loadFileUrl( + instanceManager.getIdentifier(instance)!, + url, + readAccessUrl, + ); + } + + /// Calls [loadFlutterAsset] with the ids of the provided object instances. + Future loadFlutterAssetForInstances(WKWebView instance, String key) { + return loadFlutterAsset( + instanceManager.getIdentifier(instance)!, + key, + ); + } + + /// Calls [canGoBack] with the ids of the provided object instances. + Future canGoBackForInstances(WKWebView instance) { + return canGoBack(instanceManager.getIdentifier(instance)!); + } + + /// Calls [canGoForward] with the ids of the provided object instances. + Future canGoForwardForInstances(WKWebView instance) { + return canGoForward(instanceManager.getIdentifier(instance)!); + } + + /// Calls [goBack] with the ids of the provided object instances. + Future goBackForInstances(WKWebView instance) { + return goBack(instanceManager.getIdentifier(instance)!); + } + + /// Calls [goForward] with the ids of the provided object instances. + Future goForwardForInstances(WKWebView instance) { + return goForward(instanceManager.getIdentifier(instance)!); + } + + /// Calls [reload] with the ids of the provided object instances. + Future reloadForInstances(WKWebView instance) { + return reload(instanceManager.getIdentifier(instance)!); + } + + /// Calls [getUrl] with the ids of the provided object instances. + Future getUrlForInstances(WKWebView instance) { + return getUrl(instanceManager.getIdentifier(instance)!); + } + + /// Calls [getTitle] with the ids of the provided object instances. + Future getTitleForInstances(WKWebView instance) { + return getTitle(instanceManager.getIdentifier(instance)!); + } + + /// Calls [getEstimatedProgress] with the ids of the provided object instances. + Future getEstimatedProgressForInstances(WKWebView instance) { + return getEstimatedProgress(instanceManager.getIdentifier(instance)!); + } + + /// Calls [setAllowsBackForwardNavigationGestures] with the ids of the provided object instances. + Future setAllowsBackForwardNavigationGesturesForInstances( + WKWebView instance, + bool allow, + ) { + return setAllowsBackForwardNavigationGestures( + instanceManager.getIdentifier(instance)!, + allow, + ); + } + + /// Calls [setCustomUserAgent] with the ids of the provided object instances. + Future setCustomUserAgentForInstances( + WKWebView instance, + String? userAgent, + ) { + return setCustomUserAgent( + instanceManager.getIdentifier(instance)!, + userAgent, + ); + } + + /// Calls [evaluateJavaScript] with the ids of the provided object instances. + Future evaluateJavaScriptForInstances( + WKWebView instance, + String javaScriptString, + ) async { + try { + final Object? result = await evaluateJavaScript( + instanceManager.getIdentifier(instance)!, + javaScriptString, + ); + return result; + } on PlatformException catch (exception) { + if (exception.details is! NSErrorData) { + rethrow; + } + + throw PlatformException( + code: exception.code, + message: exception.message, + stacktrace: exception.stacktrace, + details: (exception.details as NSErrorData).toNSError(), + ); + } + } + + /// Calls [setNavigationDelegate] with the ids of the provided object instances. + Future setNavigationDelegateForInstances( + WKWebView instance, + WKNavigationDelegate? delegate, + ) { + return setNavigationDelegate( + instanceManager.getIdentifier(instance)!, + delegate != null ? instanceManager.getIdentifier(delegate)! : null, + ); + } + + /// Calls [setUIDelegate] with the ids of the provided object instances. + Future setUIDelegateForInstances( + WKWebView instance, + WKUIDelegate? delegate, + ) { + return setUIDelegate( + instanceManager.getIdentifier(instance)!, + delegate != null ? instanceManager.getIdentifier(delegate)! : null, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart index 748eda3a260e..25e368221a1c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart @@ -3,17 +3,23 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:math'; import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as path; import 'package:webview_pro_platform_interface/webview_flutter_platform_interface.dart'; +import 'common/weak_reference_utils.dart'; +import 'foundation/foundation.dart'; import 'web_kit/web_kit.dart'; /// A [Widget] that displays a [WKWebView]. class WebKitWebViewWidget extends StatefulWidget { /// Constructs a [WebKitWebViewWidget]. const WebKitWebViewWidget({ + super.key, required this.creationParams, required this.callbacksHandler, required this.javascriptChannelRegistry, @@ -82,23 +88,14 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { }) : super(callbacksHandler) { _setCreationParams( creationParams, - configuration: configuration ?? - WKWebViewConfiguration( - userContentController: WKUserContentController(), - ), + configuration: configuration ?? WKWebViewConfiguration(), ); - - webView.uiDelegate = uiDelegate; - uiDelegate.onCreateWebView = ( - WKWebViewConfiguration configuration, - WKNavigationAction navigationAction, - ) { - if (!navigationAction.targetFrame.isMainFrame) { - webView.loadRequest(navigationAction.request); - } - }; } + bool _zoomEnabled = true; + bool _hasNavigationDelegate = false; + bool _progressObserverSet = false; + final Map _scriptMessageHandlers = {}; @@ -118,7 +115,76 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { /// Used to integrate custom user interface elements into web view interactions. @visibleForTesting - late final WKUIDelegate uiDelegate = webViewProxy.createUIDelgate(); + late final WKUIDelegate uiDelegate = webViewProxy.createUIDelgate( + onCreateWebView: ( + WKWebView webView, + WKWebViewConfiguration configuration, + WKNavigationAction navigationAction, + ) { + if (!navigationAction.targetFrame.isMainFrame) { + webView.loadRequest(navigationAction.request); + } + }, + ); + + /// Methods for handling navigation changes and tracking navigation requests. + @visibleForTesting + late final WKNavigationDelegate navigationDelegate = withWeakRefenceTo( + this, + (WeakReference weakReference) { + return webViewProxy.createNavigationDelegate( + didFinishNavigation: (WKWebView webView, String? url) { + weakReference.target?.callbacksHandler.onPageFinished(url ?? ''); + }, + didStartProvisionalNavigation: (WKWebView webView, String? url) { + weakReference.target?.callbacksHandler.onPageStarted(url ?? ''); + }, + decidePolicyForNavigationAction: ( + WKWebView webView, + WKNavigationAction action, + ) async { + if (weakReference.target == null) { + return WKNavigationActionPolicy.allow; + } + + if (!weakReference.target!._hasNavigationDelegate) { + return WKNavigationActionPolicy.allow; + } + + final bool allow = + await weakReference.target!.callbacksHandler.onNavigationRequest( + url: action.request.url, + isForMainFrame: action.targetFrame.isMainFrame, + ); + + return allow + ? WKNavigationActionPolicy.allow + : WKNavigationActionPolicy.cancel; + }, + didFailNavigation: (WKWebView webView, NSError error) { + weakReference.target?.callbacksHandler.onWebResourceError( + _toWebResourceError(error), + ); + }, + didFailProvisionalNavigation: (WKWebView webView, NSError error) { + weakReference.target?.callbacksHandler.onWebResourceError( + _toWebResourceError(error), + ); + }, + webViewWebContentProcessDidTerminate: (WKWebView webView) { + weakReference.target?.callbacksHandler.onWebResourceError( + WebResourceError( + errorCode: WKErrorCode.webContentProcessTerminated, + // Value from https://developer.apple.com/documentation/webkit/wkerrordomain?language=objc. + domain: 'WKErrorDomain', + description: '', + errorType: WebResourceErrorType.webContentProcessTerminated, + ), + ); + }, + ); + }, + ); Future _setCreationParams( CreationParams params, { @@ -130,9 +196,47 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { autoMediaPlaybackPolicy: params.autoMediaPlaybackPolicy, ); - webView = webViewProxy.createWebView(configuration); + webView = webViewProxy.createWebView( + configuration, + observeValue: withWeakRefenceTo( + callbacksHandler, + (WeakReference weakReference) { + return ( + String keyPath, + NSObject object, + Map change, + ) { + final double progress = + change[NSKeyValueChangeKey.newValue]! as double; + weakReference.target?.onProgress((progress * 100).round()); + }; + }, + ), + ); + + webView.setUIDelegate(uiDelegate); await addJavascriptChannels(params.javascriptChannelNames); + + webView.setNavigationDelegate(navigationDelegate); + + if (params.userAgent != null) { + webView.setCustomUserAgent(params.userAgent); + } + + if (params.webSettings != null) { + updateSettings(params.webSettings!); + } + + if (params.backgroundColor != null) { + webView.setOpaque(false); + webView.setBackgroundColor(Colors.transparent); + webView.scrollView.setBackgroundColor(params.backgroundColor); + } + + if (params.initialUrl != null) { + await loadUrl(params.initialUrl!, null); + } } void _setWebViewConfiguration( @@ -141,7 +245,7 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { required AutoMediaPlaybackPolicy autoMediaPlaybackPolicy, }) { if (allowsInlineMediaPlayback != null) { - configuration.allowsInlineMediaPlayback = allowsInlineMediaPlayback; + configuration.setAllowsInlineMediaPlayback(allowsInlineMediaPlayback); } late final bool requiresUserAction; @@ -154,11 +258,171 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { break; } - configuration.mediaTypesRequiringUserActionForPlayback = - { + configuration + .setMediaTypesRequiringUserActionForPlayback({ if (requiresUserAction) WKAudiovisualMediaType.all, if (!requiresUserAction) WKAudiovisualMediaType.none, - }; + }); + } + + @override + Future loadHtmlString(String html, {String? baseUrl}) { + return webView.loadHtmlString(html, baseUrl: baseUrl); + } + + @override + Future loadFile(String absoluteFilePath) async { + await webView.loadFileUrl( + absoluteFilePath, + readAccessUrl: path.dirname(absoluteFilePath), + ); + } + + @override + Future clearCache() { + return webView.configuration.websiteDataStore.removeDataOfTypes( + { + WKWebsiteDataType.memoryCache, + WKWebsiteDataType.diskCache, + WKWebsiteDataType.offlineWebApplicationCache, + WKWebsiteDataType.localStorage, + }, + DateTime.fromMillisecondsSinceEpoch(0), + ); + } + + @override + Future loadFlutterAsset(String key) async { + assert(key.isNotEmpty); + return webView.loadFlutterAsset(key); + } + + @override + Future loadUrl(String url, Map? headers) async { + final NSUrlRequest request = NSUrlRequest( + url: url, + allHttpHeaderFields: headers ?? {}, + ); + return webView.loadRequest(request); + } + + @override + Future loadRequest(WebViewRequest request) async { + if (!request.uri.hasScheme) { + throw ArgumentError('WebViewRequest#uri is required to have a scheme.'); + } + + final NSUrlRequest urlRequest = NSUrlRequest( + url: request.uri.toString(), + allHttpHeaderFields: request.headers, + httpMethod: describeEnum(request.method), + httpBody: request.body, + ); + + return webView.loadRequest(urlRequest); + } + + @override + Future canGoBack() => webView.canGoBack(); + + @override + Future canGoForward() => webView.canGoForward(); + + @override + Future goBack() => webView.goBack(); + + @override + Future goForward() => webView.goForward(); + + @override + Future reload() => webView.reload(); + + @override + Future evaluateJavascript(String javascript) async { + final Object? result = await webView.evaluateJavaScript(javascript); + return _asObjectiveCString(result); + } + + @override + Future runJavascript(String javascript) async { + try { + await webView.evaluateJavaScript(javascript); + } on PlatformException catch (exception) { + // WebKit will throw an error when the type of the evaluated value is + // unsupported. This also goes for `null` and `undefined` on iOS 14+. For + // example, when running a void function. For ease of use, this specific + // error is ignored when no return value is expected. + if (exception.details is! NSError || + exception.details.code != + WKErrorCode.javaScriptResultTypeIsUnsupported) { + rethrow; + } + } + } + + @override + Future runJavascriptReturningResult(String javascript) async { + final Object? result = await webView.evaluateJavaScript(javascript); + if (result == null) { + throw ArgumentError( + 'Result of JavaScript execution returned a `null` value. ' + 'Use `runJavascript` when expecting a null return value.', + ); + } + return _asObjectiveCString(result); + } + + @override + Future getTitle() => webView.getTitle(); + + @override + Future currentUrl() => webView.getUrl(); + + @override + Future scrollTo(int x, int y) async { + webView.scrollView.setContentOffset(Point( + x.toDouble(), + y.toDouble(), + )); + } + + @override + Future scrollBy(int x, int y) async { + await webView.scrollView.scrollBy(Point( + x.toDouble(), + y.toDouble(), + )); + } + + @override + Future getScrollX() async { + final Point offset = await webView.scrollView.getContentOffset(); + return offset.x.toInt(); + } + + @override + Future getScrollY() async { + final Point offset = await webView.scrollView.getContentOffset(); + return offset.y.toInt(); + } + + @override + Future updateSettings(WebSettings setting) async { + if (setting.hasNavigationDelegate != null) { + _hasNavigationDelegate = setting.hasNavigationDelegate!; + } + await Future.wait(>[ + _setUserAgent(setting.userAgent), + if (setting.hasProgressTracking != null) + _setHasProgressTracking(setting.hasProgressTracking!), + if (setting.javascriptMode != null) + _setJavaScriptMode(setting.javascriptMode!), + if (setting.zoomEnabled != null) _setZoomEnabled(setting.zoomEnabled!), + if (setting.gestureNavigationEnabled != null) + webView.setAllowsBackForwardNavigationGestures( + setting.gestureNavigationEnabled!, + ), + ]); } @override @@ -171,16 +435,22 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { ).map>( (String channelName) { final WKScriptMessageHandler handler = - webViewProxy.createScriptMessageHandler() - ..didReceiveScriptMessage = ( + webViewProxy.createScriptMessageHandler( + didReceiveScriptMessage: withWeakRefenceTo( + javascriptChannelRegistry, + (WeakReference weakReference) { + return ( WKUserContentController userContentController, WKScriptMessage message, ) { - javascriptChannelRegistry.onJavascriptChannelMessage( + weakReference.target?.onJavascriptChannelMessage( message.name, message.body!.toString(), ); }; + }, + ), + ); _scriptMessageHandlers[channelName] = handler; final String wrapperSource = @@ -210,19 +480,162 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { return; } - // WKWebView does not support removing a single user script, so this removes - // all user scripts and all message handlers and re-registers channels that - // shouldn't be removed. Note that this workaround could interfere with - // exposing support for custom scripts from applications. + await _resetUserScripts(removedJavaScriptChannels: javascriptChannelNames); + } + + Future _setHasProgressTracking(bool hasProgressTracking) async { + if (hasProgressTracking) { + _progressObserverSet = true; + await webView.addObserver( + webView, + keyPath: 'estimatedProgress', + options: { + NSKeyValueObservingOptions.newValue, + }, + ); + } else if (_progressObserverSet) { + // Calls to removeObserver before addObserver causes a crash. + _progressObserverSet = false; + await webView.removeObserver(webView, keyPath: 'estimatedProgress'); + } + } + + Future _setJavaScriptMode(JavascriptMode mode) { + switch (mode) { + case JavascriptMode.disabled: + return webView.configuration.preferences.setJavaScriptEnabled(false); + case JavascriptMode.unrestricted: + return webView.configuration.preferences.setJavaScriptEnabled(true); + } + } + + Future _setUserAgent(WebSetting userAgent) async { + if (userAgent.isPresent) { + await webView.setCustomUserAgent(userAgent.value); + } + } + + Future _setZoomEnabled(bool zoomEnabled) async { + if (_zoomEnabled == zoomEnabled) { + return; + } + + _zoomEnabled = zoomEnabled; + if (!zoomEnabled) { + return _disableZoom(); + } + + return _resetUserScripts(); + } + + Future _disableZoom() { + const WKUserScript userScript = WKUserScript( + "var meta = document.createElement('meta');\n" + "meta.name = 'viewport';\n" + "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, " + "user-scalable=no';\n" + "var head = document.getElementsByTagName('head')[0];head.appendChild(meta);", + WKUserScriptInjectionTime.atDocumentEnd, + isMainFrameOnly: true, + ); + return webView.configuration.userContentController + .addUserScript(userScript); + } + + // WkWebView does not support removing a single user script, so all user + // scripts and all message handlers are removed instead. And the JavaScript + // channels that shouldn't be removed are re-registered. Note that this + // workaround could interfere with exposing support for custom scripts from + // applications. + Future _resetUserScripts({ + Set removedJavaScriptChannels = const {}, + }) async { webView.configuration.userContentController.removeAllUserScripts(); - webView.configuration.userContentController - .removeAllScriptMessageHandlers(); + // TODO(bparrishMines): This can be replaced with + // `removeAllScriptMessageHandlers` once Dart supports runtime version + // checking. (e.g. The equivalent to @availability in Objective-C.) + _scriptMessageHandlers.keys.forEach( + webView.configuration.userContentController.removeScriptMessageHandler, + ); - javascriptChannelNames.forEach(_scriptMessageHandlers.remove); + removedJavaScriptChannels.forEach(_scriptMessageHandlers.remove); final Set remainingNames = _scriptMessageHandlers.keys.toSet(); _scriptMessageHandlers.clear(); - await addJavascriptChannels(remainingNames); + await Future.wait(>[ + addJavascriptChannels(remainingNames), + // Zoom is disabled with a WKUserScript, so this adds it back if it was + // removed above. + if (!_zoomEnabled) _disableZoom(), + ]); + } + + static WebResourceError _toWebResourceError(NSError error) { + WebResourceErrorType? errorType; + + switch (error.code) { + case WKErrorCode.unknown: + errorType = WebResourceErrorType.unknown; + break; + case WKErrorCode.webContentProcessTerminated: + errorType = WebResourceErrorType.webContentProcessTerminated; + break; + case WKErrorCode.webViewInvalidated: + errorType = WebResourceErrorType.webViewInvalidated; + break; + case WKErrorCode.javaScriptExceptionOccurred: + errorType = WebResourceErrorType.javaScriptExceptionOccurred; + break; + case WKErrorCode.javaScriptResultTypeIsUnsupported: + errorType = WebResourceErrorType.javaScriptResultTypeIsUnsupported; + break; + } + + return WebResourceError( + errorCode: error.code, + domain: error.domain, + description: error.localizedDescription, + errorType: errorType, + ); + } + + // The legacy implementation of webview_flutter_wkwebview would convert + // objects to strings before returning them to Dart. This method attempts + // to converts Dart objects to Strings the way it is done in Objective-C + // to avoid breaking users expecting the same String format. + // TODO(bparrishMines): Remove this method with the next breaking change. + // See https://github.com/flutter/flutter/issues/107491 + String _asObjectiveCString(Object? value, {bool inContainer = false}) { + if (value == null) { + // An NSNull inside an NSArray or NSDictionary is represented as a String + // differently than a nil. + if (inContainer) { + return '""'; + } + return '(null)'; + } else if (value is bool) { + return value ? '1' : '0'; + } else if (value is double && value.truncate() == value) { + return value.truncate().toString(); + } else if (value is List) { + final List stringValues = []; + for (final Object? listValue in value) { + stringValues.add(_asObjectiveCString(listValue, inContainer: true)); + } + return '(${stringValues.join(',')})'; + } else if (value is Map) { + final List stringValues = []; + for (final MapEntry entry in value.entries) { + stringValues.add( + '${_asObjectiveCString(entry.key, inContainer: true)} ' + '= ' + '${_asObjectiveCString(entry.value, inContainer: true)}', + ); + } + return '{${stringValues.join(';')}}'; + } + + return value.toString(); } } @@ -235,17 +648,66 @@ class WebViewWidgetProxy { const WebViewWidgetProxy(); /// Constructs a [WKWebView]. - WKWebView createWebView(WKWebViewConfiguration configuration) { - return WKWebView(configuration); + WKWebView createWebView( + WKWebViewConfiguration configuration, { + void Function( + String keyPath, + NSObject object, + Map change, + )? + observeValue, + }) { + return WKWebView(configuration, observeValue: observeValue); } /// Constructs a [WKScriptMessageHandler]. - WKScriptMessageHandler createScriptMessageHandler() { - return WKScriptMessageHandler(); + WKScriptMessageHandler createScriptMessageHandler({ + required void Function( + WKUserContentController userContentController, + WKScriptMessage message, + ) + didReceiveScriptMessage, + }) { + return WKScriptMessageHandler( + didReceiveScriptMessage: didReceiveScriptMessage, + ); } /// Constructs a [WKUIDelegate]. - WKUIDelegate createUIDelgate() { - return WKUIDelegate(); + WKUIDelegate createUIDelgate({ + void Function( + WKWebView webView, + WKWebViewConfiguration configuration, + WKNavigationAction navigationAction, + )? + onCreateWebView, + }) { + return WKUIDelegate(onCreateWebView: onCreateWebView); + } + + /// Constructs a [WKNavigationDelegate]. + WKNavigationDelegate createNavigationDelegate({ + void Function(WKWebView webView, String? url)? didFinishNavigation, + void Function(WKWebView webView, String? url)? + didStartProvisionalNavigation, + Future Function( + WKWebView webView, + WKNavigationAction navigationAction, + )? + decidePolicyForNavigationAction, + void Function(WKWebView webView, NSError error)? didFailNavigation, + void Function(WKWebView webView, NSError error)? + didFailProvisionalNavigation, + void Function(WKWebView webView)? webViewWebContentProcessDidTerminate, + }) { + return WKNavigationDelegate( + didFinishNavigation: didFinishNavigation, + didStartProvisionalNavigation: didStartProvisionalNavigation, + decidePolicyForNavigationAction: decidePolicyForNavigationAction, + didFailNavigation: didFailNavigation, + didFailProvisionalNavigation: didFailProvisionalNavigation, + webViewWebContentProcessDidTerminate: + webViewWebContentProcessDidTerminate, + ); } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart index 5cfa99666628..ec6869c66d01 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart @@ -10,6 +10,9 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:webview_pro_platform_interface/webview_flutter_platform_interface.dart'; +import 'foundation/foundation.dart'; +import 'web_kit_webview_widget.dart'; + /// Builds an iOS webview. /// /// This is used as the default implementation for [WebView.platform] on iOS. It uses @@ -25,22 +28,24 @@ class CupertinoWebView implements WebViewPlatform { WebViewPlatformCreatedCallback? onWebViewPlatformCreated, Set>? gestureRecognizers, }) { - return UiKitView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated(MethodChannelWebViewPlatform( - id, - webViewPlatformCallbacksHandler, - javascriptChannelRegistry, - )); + return WebKitWebViewWidget( + creationParams: creationParams, + callbacksHandler: webViewPlatformCallbacksHandler, + javascriptChannelRegistry: javascriptChannelRegistry, + onBuildWidget: (WebKitWebViewPlatformController controller) { + return UiKitView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + if (onWebViewPlatformCreated != null) { + onWebViewPlatformCreated(controller); + } + }, + gestureRecognizers: gestureRecognizers, + creationParams: + NSObject.globalInstanceManager.getIdentifier(controller.webView), + creationParamsCodec: const StandardMessageCodec(), + ); }, - gestureRecognizers: gestureRecognizers, - creationParams: - MethodChannelWebViewPlatform.creationParamsToMap(creationParams), - creationParamsCodec: const StandardMessageCodec(), ); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart index 07ef22edbcab..970376ff3a6b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart @@ -4,10 +4,26 @@ import 'package:webview_pro_platform_interface/webview_flutter_platform_interface.dart'; -/// Handles all cookie operations for the current platform. +import 'foundation/foundation.dart'; +import 'web_kit/web_kit.dart'; + +/// Handles all cookie operations for the WebView platform. class WKWebViewCookieManager extends WebViewCookieManagerPlatform { + /// Constructs a [WKWebViewCookieManager]. + WKWebViewCookieManager({WKWebsiteDataStore? websiteDataStore}) + : websiteDataStore = + websiteDataStore ?? WKWebsiteDataStore.defaultDataStore; + + /// Manages stored data for [WKWebView]s. + final WKWebsiteDataStore websiteDataStore; + @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); + Future clearCookies() async { + return websiteDataStore.removeDataOfTypes( + {WKWebsiteDataType.cookies}, + DateTime.fromMillisecondsSinceEpoch(0), + ); + } @override Future setCookie(WebViewCookie cookie) { @@ -15,16 +31,25 @@ class WKWebViewCookieManager extends WebViewCookieManagerPlatform { throw ArgumentError( 'The path property for the provided cookie was not given a legal value.'); } - return MethodChannelWebViewPlatform.setCookie(cookie); + + return websiteDataStore.httpCookieStore.setCookie( + NSHttpCookie.withProperties( + { + NSHttpCookiePropertyKey.name: cookie.name, + NSHttpCookiePropertyKey.value: cookie.value, + NSHttpCookiePropertyKey.domain: cookie.domain, + NSHttpCookiePropertyKey.path: cookie.path, + }, + ), + ); } bool _isValidPath(String path) { // Permitted ranges based on RFC6265bis: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 - for (final int char in path.codeUnits) { - if ((char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E)) { - return false; - } - } - return true; + return !path.codeUnits.any( + (int char) { + return (char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E); + }, + ); } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart new file mode 100644 index 000000000000..c20a10ebfadd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart @@ -0,0 +1,620 @@ +// 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/common/web_kit.pigeon.dart', + dartTestOut: 'test/src/common/test_web_kit.pigeon.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', + 'found in the LICENSE file.', + ]), + objcHeaderOut: 'ios/Classes/FWFGeneratedWebKitApis.h', + objcSourceOut: 'ios/Classes/FWFGeneratedWebKitApis.m', + objcOptions: ObjcOptions( + header: 'ios/Classes/FWFGeneratedWebKitApis.h', + prefix: 'FWF', + copyrightHeader: [ + '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.', + ], + ), + ), +) + +/// Mirror of NSKeyValueObservingOptions. +/// +/// See https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions?language=objc. +enum NSKeyValueObservingOptionsEnum { + newValue, + oldValue, + initialValue, + priorNotification, +} + +// 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 NSKeyValueObservingOptionsEnumData { + late NSKeyValueObservingOptionsEnum value; +} + +/// Mirror of NSKeyValueChange. +/// +/// See https://developer.apple.com/documentation/foundation/nskeyvaluechange?language=objc. +enum NSKeyValueChangeEnum { + setting, + insertion, + removal, + replacement, +} + +// 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 NSKeyValueChangeEnumData { + late NSKeyValueChangeEnum value; +} + +/// Mirror of NSKeyValueChangeKey. +/// +/// See https://developer.apple.com/documentation/foundation/nskeyvaluechangekey?language=objc. +enum NSKeyValueChangeKeyEnum { + indexes, + kind, + newValue, + notificationIsPrior, + oldValue, +} + +// 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 NSKeyValueChangeKeyEnumData { + late NSKeyValueChangeKeyEnum value; +} + +/// Mirror of WKUserScriptInjectionTime. +/// +/// See https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime?language=objc. +enum WKUserScriptInjectionTimeEnum { + atDocumentStart, + atDocumentEnd, +} + +// 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 WKUserScriptInjectionTimeEnumData { + late WKUserScriptInjectionTimeEnum value; +} + +/// Mirror of WKAudiovisualMediaTypes. +/// +/// See [WKAudiovisualMediaTypes](https://developer.apple.com/documentation/webkit/wkaudiovisualmediatypes?language=objc). +enum WKAudiovisualMediaTypeEnum { + none, + audio, + video, + all, +} + +// 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 WKAudiovisualMediaTypeEnumData { + late WKAudiovisualMediaTypeEnum value; +} + +/// Mirror of WKWebsiteDataTypes. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebsitedatarecord/data_store_record_types?language=objc. +enum WKWebsiteDataTypeEnum { + cookies, + memoryCache, + diskCache, + offlineWebApplicationCache, + localStorage, + sessionStorage, + webSQLDatabases, + indexedDBDatabases, +} + +// 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 WKWebsiteDataTypeEnumData { + late WKWebsiteDataTypeEnum value; +} + +/// Mirror of WKNavigationActionPolicy. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationactionpolicy?language=objc. +enum WKNavigationActionPolicyEnum { + allow, + cancel, +} + +// 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 WKNavigationActionPolicyEnumData { + late WKNavigationActionPolicyEnum value; +} + +/// Mirror of NSHTTPCookiePropertyKey. +/// +/// See https://developer.apple.com/documentation/foundation/nshttpcookiepropertykey. +enum NSHttpCookiePropertyKeyEnum { + comment, + commentUrl, + discard, + domain, + expires, + maximumAge, + name, + originUrl, + path, + port, + sameSitePolicy, + secure, + value, + version, +} + +// 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 NSHttpCookiePropertyKeyEnumData { + late NSHttpCookiePropertyKeyEnum value; +} + +/// Mirror of NSURLRequest. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlrequest?language=objc. +class NSUrlRequestData { + late String url; + late String? httpMethod; + late Uint8List? httpBody; + late Map allHttpHeaderFields; +} + +/// Mirror of WKUserScript. +/// +/// See https://developer.apple.com/documentation/webkit/wkuserscript?language=objc. +class WKUserScriptData { + late String source; + late WKUserScriptInjectionTimeEnumData? injectionTime; + late bool isMainFrameOnly; +} + +/// Mirror of WKNavigationAction. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationaction. +class WKNavigationActionData { + late NSUrlRequestData request; + late WKFrameInfoData targetFrame; +} + +/// Mirror of WKFrameInfo. +/// +/// See https://developer.apple.com/documentation/webkit/wkframeinfo?language=objc. +class WKFrameInfoData { + late bool isMainFrame; +} + +/// Mirror of NSError. +/// +/// See https://developer.apple.com/documentation/foundation/nserror?language=objc. +class NSErrorData { + late int code; + late String domain; + late String localizedDescription; +} + +/// Mirror of WKScriptMessage. +/// +/// See https://developer.apple.com/documentation/webkit/wkscriptmessage?language=objc. +class WKScriptMessageData { + late String name; + late Object? body; +} + +/// Mirror of NSHttpCookieData. +/// +/// See https://developer.apple.com/documentation/foundation/nshttpcookie?language=objc. +class NSHttpCookieData { + // TODO(bparrishMines): Change to a map when Objective-C data classes conform + // to `NSCopying`. See https://github.com/flutter/flutter/issues/103383. + // `NSDictionary`s are unable to use data classes as keys because they don't + // conform to `NSCopying`. This splits the map of properties into a list of + // keys and values with the ordered maintained. + late List propertyKeys; + late List propertyValues; +} + +/// Mirror of WKWebsiteDataStore. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebsitedatastore?language=objc. +@HostApi(dartHostTestHandler: 'TestWKWebsiteDataStoreHostApi') +abstract class WKWebsiteDataStoreHostApi { + @ObjCSelector( + 'createFromWebViewConfigurationWithIdentifier:configurationIdentifier:', + ) + void createFromWebViewConfiguration( + int identifier, + int configurationIdentifier, + ); + + @ObjCSelector('createDefaultDataStoreWithIdentifier:') + void createDefaultDataStore(int identifier); + + @ObjCSelector( + 'removeDataFromDataStoreWithIdentifier:ofTypes:modifiedSince:', + ) + @async + bool removeDataOfTypes( + int identifier, + List dataTypes, + double modificationTimeInSecondsSinceEpoch, + ); +} + +/// Mirror of UIView. +/// +/// See https://developer.apple.com/documentation/uikit/uiview?language=objc. +@HostApi(dartHostTestHandler: 'TestUIViewHostApi') +abstract class UIViewHostApi { + @ObjCSelector('setBackgroundColorForViewWithIdentifier:toValue:') + void setBackgroundColor(int identifier, int? value); + + @ObjCSelector('setOpaqueForViewWithIdentifier:isOpaque:') + void setOpaque(int identifier, bool opaque); +} + +/// Mirror of UIScrollView. +/// +/// See https://developer.apple.com/documentation/uikit/uiscrollview?language=objc. +@HostApi(dartHostTestHandler: 'TestUIScrollViewHostApi') +abstract class UIScrollViewHostApi { + @ObjCSelector('createFromWebViewWithIdentifier:webViewIdentifier:') + void createFromWebView(int identifier, int webViewIdentifier); + + @ObjCSelector('contentOffsetForScrollViewWithIdentifier:') + List getContentOffset(int identifier); + + @ObjCSelector('scrollByForScrollViewWithIdentifier:x:y:') + void scrollBy(int identifier, double x, double y); + + @ObjCSelector('setContentOffsetForScrollViewWithIdentifier:toX:y:') + void setContentOffset(int identifier, double x, double y); +} + +/// Mirror of WKWebViewConfiguration. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebviewconfiguration?language=objc. +@HostApi(dartHostTestHandler: 'TestWKWebViewConfigurationHostApi') +abstract class WKWebViewConfigurationHostApi { + @ObjCSelector('createWithIdentifier:') + void create(int identifier); + + @ObjCSelector('createFromWebViewWithIdentifier:webViewIdentifier:') + void createFromWebView(int identifier, int webViewIdentifier); + + @ObjCSelector( + 'setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:isAllowed:', + ) + void setAllowsInlineMediaPlayback(int identifier, bool allow); + + @ObjCSelector( + 'setMediaTypesRequiresUserActionForConfigurationWithIdentifier:forTypes:', + ) + void setMediaTypesRequiringUserActionForPlayback( + int identifier, + List types, + ); +} + +/// Handles callbacks from an WKWebViewConfiguration instance. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebviewconfiguration?language=objc. +@FlutterApi() +abstract class WKWebViewConfigurationFlutterApi { + @ObjCSelector('createWithIdentifier:') + void create(int identifier); +} + +/// Mirror of WKUserContentController. +/// +/// See https://developer.apple.com/documentation/webkit/wkusercontentcontroller?language=objc. +@HostApi(dartHostTestHandler: 'TestWKUserContentControllerHostApi') +abstract class WKUserContentControllerHostApi { + @ObjCSelector( + 'createFromWebViewConfigurationWithIdentifier:configurationIdentifier:', + ) + void createFromWebViewConfiguration( + int identifier, + int configurationIdentifier, + ); + + @ObjCSelector( + 'addScriptMessageHandlerForControllerWithIdentifier:handlerIdentifier:ofName:', + ) + void addScriptMessageHandler( + int identifier, + int handlerIdentifier, + String name, + ); + + @ObjCSelector('removeScriptMessageHandlerForControllerWithIdentifier:name:') + void removeScriptMessageHandler(int identifier, String name); + + @ObjCSelector('removeAllScriptMessageHandlersForControllerWithIdentifier:') + void removeAllScriptMessageHandlers(int identifier); + + @ObjCSelector('addUserScriptForControllerWithIdentifier:userScript:') + void addUserScript(int identifier, WKUserScriptData userScript); + + @ObjCSelector('removeAllUserScriptsForControllerWithIdentifier:') + void removeAllUserScripts(int identifier); +} + +/// Mirror of WKUserPreferences. +/// +/// See https://developer.apple.com/documentation/webkit/wkpreferences?language=objc. +@HostApi(dartHostTestHandler: 'TestWKPreferencesHostApi') +abstract class WKPreferencesHostApi { + @ObjCSelector( + 'createFromWebViewConfigurationWithIdentifier:configurationIdentifier:', + ) + void createFromWebViewConfiguration( + int identifier, + int configurationIdentifier, + ); + + @ObjCSelector('setJavaScriptEnabledForPreferencesWithIdentifier:isEnabled:') + void setJavaScriptEnabled(int identifier, bool enabled); +} + +/// Mirror of WKScriptMessageHandler. +/// +/// See https://developer.apple.com/documentation/webkit/wkscriptmessagehandler?language=objc. +@HostApi(dartHostTestHandler: 'TestWKScriptMessageHandlerHostApi') +abstract class WKScriptMessageHandlerHostApi { + @ObjCSelector('createWithIdentifier:') + void create(int identifier); +} + +/// Handles callbacks from an WKScriptMessageHandler instance. +/// +/// See https://developer.apple.com/documentation/webkit/wkscriptmessagehandler?language=objc. +@FlutterApi() +abstract class WKScriptMessageHandlerFlutterApi { + @ObjCSelector( + 'didReceiveScriptMessageForHandlerWithIdentifier:userContentControllerIdentifier:message:', + ) + void didReceiveScriptMessage( + int identifier, + int userContentControllerIdentifier, + WKScriptMessageData message, + ); +} + +/// Mirror of WKNavigationDelegate. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationdelegate?language=objc. +@HostApi(dartHostTestHandler: 'TestWKNavigationDelegateHostApi') +abstract class WKNavigationDelegateHostApi { + @ObjCSelector('createWithIdentifier:') + void create(int identifier); +} + +/// Handles callbacks from an WKNavigationDelegate instance. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationdelegate?language=objc. +@FlutterApi() +abstract class WKNavigationDelegateFlutterApi { + @ObjCSelector( + 'didFinishNavigationForDelegateWithIdentifier:webViewIdentifier:URL:', + ) + void didFinishNavigation( + int identifier, + int webViewIdentifier, + String? url, + ); + + @ObjCSelector( + 'didStartProvisionalNavigationForDelegateWithIdentifier:webViewIdentifier:URL:', + ) + void didStartProvisionalNavigation( + int identifier, + int webViewIdentifier, + String? url, + ); + + @ObjCSelector( + 'decidePolicyForNavigationActionForDelegateWithIdentifier:webViewIdentifier:navigationAction:', + ) + @async + WKNavigationActionPolicyEnumData decidePolicyForNavigationAction( + int identifier, + int webViewIdentifier, + WKNavigationActionData navigationAction, + ); + + @ObjCSelector( + 'didFailNavigationForDelegateWithIdentifier:webViewIdentifier:error:', + ) + void didFailNavigation( + int identifier, + int webViewIdentifier, + NSErrorData error, + ); + + @ObjCSelector( + 'didFailProvisionalNavigationForDelegateWithIdentifier:webViewIdentifier:error:', + ) + void didFailProvisionalNavigation( + int identifier, + int webViewIdentifier, + NSErrorData error, + ); + + @ObjCSelector( + 'webViewWebContentProcessDidTerminateForDelegateWithIdentifier:webViewIdentifier:', + ) + void webViewWebContentProcessDidTerminate( + int identifier, + int webViewIdentifier, + ); +} + +/// Mirror of NSObject. +/// +/// See https://developer.apple.com/documentation/objectivec/nsobject. +@HostApi(dartHostTestHandler: 'TestNSObjectHostApi') +abstract class NSObjectHostApi { + @ObjCSelector('disposeObjectWithIdentifier:') + void dispose(int identifier); + + @ObjCSelector( + 'addObserverForObjectWithIdentifier:observerIdentifier:keyPath:options:', + ) + void addObserver( + int identifier, + int observerIdentifier, + String keyPath, + List options, + ); + + @ObjCSelector( + 'removeObserverForObjectWithIdentifier:observerIdentifier:keyPath:', + ) + void removeObserver(int identifier, int observerIdentifier, String keyPath); +} + +/// Handles callbacks from an NSObject instance. +/// +/// See https://developer.apple.com/documentation/objectivec/nsobject. +@FlutterApi() +abstract class NSObjectFlutterApi { + @ObjCSelector( + 'observeValueForObjectWithIdentifier:keyPath:objectIdentifier:changeKeys:changeValues:', + ) + void observeValue( + int identifier, + String keyPath, + int objectIdentifier, + // TODO(bparrishMines): Change to a map when Objective-C data classes conform + // to `NSCopying`. See https://github.com/flutter/flutter/issues/103383. + // `NSDictionary`s are unable to use data classes as keys because they don't + // conform to `NSCopying`. This splits the map of properties into a list of + // keys and values with the ordered maintained. + List changeKeys, + List changeValues, + ); + + @ObjCSelector('disposeObjectWithIdentifier:') + void dispose(int identifier); +} + +/// Mirror of WKWebView. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebview?language=objc. +@HostApi(dartHostTestHandler: 'TestWKWebViewHostApi') +abstract class WKWebViewHostApi { + @ObjCSelector('createWithIdentifier:configurationIdentifier:') + void create(int identifier, int configurationIdentifier); + + @ObjCSelector('setUIDelegateForWebViewWithIdentifier:delegateIdentifier:') + void setUIDelegate(int identifier, int? uiDelegateIdentifier); + + @ObjCSelector( + 'setNavigationDelegateForWebViewWithIdentifier:delegateIdentifier:', + ) + void setNavigationDelegate(int identifier, int? navigationDelegateIdentifier); + + @ObjCSelector('URLForWebViewWithIdentifier:') + String? getUrl(int identifier); + + @ObjCSelector('estimatedProgressForWebViewWithIdentifier:') + double getEstimatedProgress(int identifier); + + @ObjCSelector('loadRequestForWebViewWithIdentifier:request:') + void loadRequest(int identifier, NSUrlRequestData request); + + @ObjCSelector('loadHTMLForWebViewWithIdentifier:HTMLString:baseURL:') + void loadHtmlString(int identifier, String string, String? baseUrl); + + @ObjCSelector('loadFileForWebViewWithIdentifier:fileURL:readAccessURL:') + void loadFileUrl(int identifier, String url, String readAccessUrl); + + @ObjCSelector('loadAssetForWebViewWithIdentifier:assetKey:') + void loadFlutterAsset(int identifier, String key); + + @ObjCSelector('canGoBackForWebViewWithIdentifier:') + bool canGoBack(int identifier); + + @ObjCSelector('canGoForwardForWebViewWithIdentifier:') + bool canGoForward(int identifier); + + @ObjCSelector('goBackForWebViewWithIdentifier:') + void goBack(int identifier); + + @ObjCSelector('goForwardForWebViewWithIdentifier:') + void goForward(int identifier); + + @ObjCSelector('reloadWebViewWithIdentifier:') + void reload(int identifier); + + @ObjCSelector('titleForWebViewWithIdentifier:') + String? getTitle(int identifier); + + @ObjCSelector('setAllowsBackForwardForWebViewWithIdentifier:isAllowed:') + void setAllowsBackForwardNavigationGestures(int identifier, bool allow); + + @ObjCSelector('setUserAgentForWebViewWithIdentifier:userAgent:') + void setCustomUserAgent(int identifier, String? userAgent); + + @ObjCSelector('evaluateJavaScriptForWebViewWithIdentifier:javaScriptString:') + @async + Object? evaluateJavaScript(int identifier, String javaScriptString); +} + +/// Mirror of WKUIDelegate. +/// +/// See https://developer.apple.com/documentation/webkit/wkuidelegate?language=objc. +@HostApi(dartHostTestHandler: 'TestWKUIDelegateHostApi') +abstract class WKUIDelegateHostApi { + @ObjCSelector('createWithIdentifier:') + void create(int identifier); +} + +/// Handles callbacks from an WKUIDelegate instance. +/// +/// See https://developer.apple.com/documentation/webkit/wkuidelegate?language=objc. +@FlutterApi() +abstract class WKUIDelegateFlutterApi { + @ObjCSelector( + 'onCreateWebViewForDelegateWithIdentifier:webViewIdentifier:configurationIdentifier:navigationAction:', + ) + void onCreateWebView( + int identifier, + int webViewIdentifier, + int configurationIdentifier, + WKNavigationActionData navigationAction, + ); +} + +/// Mirror of WKHttpCookieStore. +/// +/// See https://developer.apple.com/documentation/webkit/wkhttpcookiestore?language=objc. +@HostApi(dartHostTestHandler: 'TestWKHttpCookieStoreHostApi') +abstract class WKHttpCookieStoreHostApi { + @ObjCSelector('createFromWebsiteDataStoreWithIdentifier:dataStoreIdentifier:') + void createFromWebsiteDataStore( + int identifier, + int websiteDataStoreIdentifier, + ); + + @ObjCSelector('setCookieForStoreWithIdentifier:cookie:') + @async + void setCookie(int identifier, NSHttpCookieData cookie); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index ea5b9da1203e..988b96214bea 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,11 +2,11 @@ name: webview_pro_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/wenzhiming/flutter-plugins/tree/dev/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/wenzhiming/flutter-plugins/issues -version: 2.7.1+3 +version: 2.9.5+1 environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + sdk: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" flutter: plugin: @@ -27,5 +27,5 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - mockito: ^5.0.16 - pedantic: ^1.10.0 + mockito: ^5.1.0 + pigeon: ^3.0.3 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/instance_manager_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/instance_manager_test.dart new file mode 100644 index 000000000000..2fc68a489b6a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/instance_manager_test.dart @@ -0,0 +1,153 @@ +// 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_wkwebview/src/common/instance_manager.dart'; + +void main() { + group('InstanceManager', () { + test('addHostCreatedInstance', () { + final CopyableObject object = CopyableObject(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance(object, 0); + + expect(instanceManager.getIdentifier(object), 0); + expect( + instanceManager.getInstanceWithWeakReference(0), + object, + ); + }); + + test('addHostCreatedInstance prevents already used objects and ids', () { + final CopyableObject object = CopyableObject(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance(object, 0); + + expect( + () => instanceManager.addHostCreatedInstance(object, 0), + throwsAssertionError, + ); + + expect( + () => instanceManager.addHostCreatedInstance(CopyableObject(), 0), + throwsAssertionError, + ); + }); + + test('addFlutterCreatedInstance', () { + final CopyableObject object = CopyableObject(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addDartCreatedInstance(object); + + final int? instanceId = instanceManager.getIdentifier(object); + expect(instanceId, isNotNull); + expect( + instanceManager.getInstanceWithWeakReference(instanceId!), + object, + ); + }); + + test('removeWeakReference', () { + final CopyableObject object = CopyableObject(); + + int? weakInstanceId; + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (int instanceId) { + weakInstanceId = instanceId; + }); + + instanceManager.addHostCreatedInstance(object, 0); + + expect(instanceManager.removeWeakReference(object), 0); + expect( + instanceManager.getInstanceWithWeakReference(0), + isA(), + ); + expect(weakInstanceId, 0); + }); + + test('removeWeakReference removes only weak reference', () { + final CopyableObject object = CopyableObject(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance(object, 0); + + expect(instanceManager.removeWeakReference(object), 0); + final CopyableObject copy = instanceManager.getInstanceWithWeakReference( + 0, + )!; + expect(identical(object, copy), isFalse); + }); + + test('removeStrongReference', () { + final CopyableObject object = CopyableObject(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance(object, 0); + instanceManager.removeWeakReference(object); + expect(instanceManager.remove(0), isA()); + expect(instanceManager.containsIdentifier(0), isFalse); + }); + + test('removeStrongReference removes only strong reference', () { + final CopyableObject object = CopyableObject(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance(object, 0); + expect(instanceManager.remove(0), isA()); + expect( + instanceManager.getInstanceWithWeakReference(0), + object, + ); + }); + + test('getInstance can add a new weak reference', () { + final CopyableObject object = CopyableObject(); + + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (_) {}); + + instanceManager.addHostCreatedInstance(object, 0); + instanceManager.removeWeakReference(object); + + final CopyableObject newWeakCopy = + instanceManager.getInstanceWithWeakReference( + 0, + )!; + expect(identical(object, newWeakCopy), isFalse); + }); + }); +} + +class CopyableObject with Copyable { + @override + Copyable copy() { + return CopyableObject(); + } + + @override + int get hashCode { + return 0; + } + + @override + bool operator ==(Object other) { + return other is CopyableObject; + } +} 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.pigeon.dart new file mode 100644 index 000000000000..a9e5c8bb1db4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.pigeon.dart @@ -0,0 +1,1466 @@ +// 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.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis +// ignore_for_file: avoid_relative_lib_imports +// @dart = 2.12 +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:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart'; + +class _TestWKWebsiteDataStoreHostApiCodec extends StandardMessageCodec { + const _TestWKWebsiteDataStoreHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WKWebsiteDataTypeEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WKWebsiteDataTypeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestWKWebsiteDataStoreHostApi { + static const MessageCodec codec = + _TestWKWebsiteDataStoreHostApiCodec(); + + void createFromWebViewConfiguration( + int identifier, int configurationIdentifier); + void createDefaultDataStore(int identifier); + Future removeDataOfTypes( + int identifier, + List dataTypes, + double modificationTimeInSecondsSinceEpoch); + static void setup(TestWKWebsiteDataStoreHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration 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.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration was null, expected non-null int.'); + final int? arg_configurationIdentifier = (args[1] as int?); + assert(arg_configurationIdentifier != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration was null, expected non-null int.'); + api.createFromWebViewConfiguration( + arg_identifier!, arg_configurationIdentifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createDefaultDataStore', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createDefaultDataStore 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.WKWebsiteDataStoreHostApi.createDefaultDataStore was null, expected non-null int.'); + api.createDefaultDataStore(arg_identifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes 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.WKWebsiteDataStoreHostApi.removeDataOfTypes was null, expected non-null int.'); + final List? arg_dataTypes = + (args[1] as List?)?.cast(); + assert(arg_dataTypes != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes was null, expected non-null List.'); + final double? arg_modificationTimeInSecondsSinceEpoch = + (args[2] as double?); + assert(arg_modificationTimeInSecondsSinceEpoch != null, + 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes was null, expected non-null double.'); + final bool output = await api.removeDataOfTypes(arg_identifier!, + arg_dataTypes!, arg_modificationTimeInSecondsSinceEpoch!); + return {'result': output}; + }); + } + } + } +} + +class _TestUIViewHostApiCodec extends StandardMessageCodec { + const _TestUIViewHostApiCodec(); +} + +abstract class TestUIViewHostApi { + static const MessageCodec codec = _TestUIViewHostApiCodec(); + + void setBackgroundColor(int identifier, int? value); + void setOpaque(int identifier, bool opaque); + static void setup(TestUIViewHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIViewHostApi.setBackgroundColor', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UIViewHostApi.setBackgroundColor 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.UIViewHostApi.setBackgroundColor was null, expected non-null int.'); + final int? arg_value = (args[1] as int?); + api.setBackgroundColor(arg_identifier!, arg_value); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIViewHostApi.setOpaque', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UIViewHostApi.setOpaque 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.UIViewHostApi.setOpaque was null, expected non-null int.'); + final bool? arg_opaque = (args[1] as bool?); + assert(arg_opaque != null, + 'Argument for dev.flutter.pigeon.UIViewHostApi.setOpaque was null, expected non-null bool.'); + api.setOpaque(arg_identifier!, arg_opaque!); + return {}; + }); + } + } + } +} + +class _TestUIScrollViewHostApiCodec extends StandardMessageCodec { + const _TestUIScrollViewHostApiCodec(); +} + +abstract class TestUIScrollViewHostApi { + static const MessageCodec codec = _TestUIScrollViewHostApiCodec(); + + void createFromWebView(int identifier, int webViewIdentifier); + List getContentOffset(int identifier); + void scrollBy(int identifier, double x, double y); + void setContentOffset(int identifier, double x, double y); + static void setup(TestUIScrollViewHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.createFromWebView', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.createFromWebView 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.UIScrollViewHostApi.createFromWebView was null, expected non-null int.'); + final int? arg_webViewIdentifier = (args[1] as int?); + assert(arg_webViewIdentifier != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.createFromWebView was null, expected non-null int.'); + api.createFromWebView(arg_identifier!, arg_webViewIdentifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.getContentOffset', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.getContentOffset 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.UIScrollViewHostApi.getContentOffset was null, expected non-null int.'); + final List output = api.getContentOffset(arg_identifier!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.scrollBy', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.scrollBy 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.UIScrollViewHostApi.scrollBy was null, expected non-null int.'); + final double? arg_x = (args[1] as double?); + assert(arg_x != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.scrollBy was null, expected non-null double.'); + final double? arg_y = (args[2] as double?); + assert(arg_y != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.scrollBy was null, expected non-null double.'); + api.scrollBy(arg_identifier!, arg_x!, arg_y!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset 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.UIScrollViewHostApi.setContentOffset was null, expected non-null int.'); + final double? arg_x = (args[1] as double?); + assert(arg_x != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset was null, expected non-null double.'); + final double? arg_y = (args[2] as double?); + assert(arg_y != null, + 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset was null, expected non-null double.'); + api.setContentOffset(arg_identifier!, arg_x!, arg_y!); + return {}; + }); + } + } + } +} + +class _TestWKWebViewConfigurationHostApiCodec extends StandardMessageCodec { + const _TestWKWebViewConfigurationHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WKAudiovisualMediaTypeEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WKAudiovisualMediaTypeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestWKWebViewConfigurationHostApi { + static const MessageCodec codec = + _TestWKWebViewConfigurationHostApiCodec(); + + void create(int identifier); + void createFromWebView(int identifier, int webViewIdentifier); + void setAllowsInlineMediaPlayback(int identifier, bool allow); + void setMediaTypesRequiringUserActionForPlayback( + int identifier, List types); + static void setup(TestWKWebViewConfigurationHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.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.WKWebViewConfigurationHostApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView was null, expected non-null int.'); + final int? arg_webViewIdentifier = (args[1] as int?); + assert(arg_webViewIdentifier != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView was null, expected non-null int.'); + api.createFromWebView(arg_identifier!, arg_webViewIdentifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback was null, expected non-null int.'); + final bool? arg_allow = (args[1] as bool?); + assert(arg_allow != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback was null, expected non-null bool.'); + api.setAllowsInlineMediaPlayback(arg_identifier!, arg_allow!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback was null, expected non-null int.'); + final List? arg_types = + (args[1] as List?) + ?.cast(); + assert(arg_types != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback was null, expected non-null List.'); + api.setMediaTypesRequiringUserActionForPlayback( + arg_identifier!, arg_types!); + return {}; + }); + } + } + } +} + +class _TestWKUserContentControllerHostApiCodec extends StandardMessageCodec { + const _TestWKUserContentControllerHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is WKUserScriptData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptInjectionTimeEnumData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WKUserScriptData.decode(readValue(buffer)!); + + case 129: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestWKUserContentControllerHostApi { + static const MessageCodec codec = + _TestWKUserContentControllerHostApiCodec(); + + void createFromWebViewConfiguration( + int identifier, int configurationIdentifier); + void addScriptMessageHandler( + int identifier, int handlerIdentifier, String name); + void removeScriptMessageHandler(int identifier, String name); + void removeAllScriptMessageHandlers(int identifier); + void addUserScript(int identifier, WKUserScriptData userScript); + void removeAllUserScripts(int identifier); + static void setup(TestWKUserContentControllerHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.createFromWebViewConfiguration', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.createFromWebViewConfiguration 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.WKUserContentControllerHostApi.createFromWebViewConfiguration was null, expected non-null int.'); + final int? arg_configurationIdentifier = (args[1] as int?); + assert(arg_configurationIdentifier != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.createFromWebViewConfiguration was null, expected non-null int.'); + api.createFromWebViewConfiguration( + arg_identifier!, arg_configurationIdentifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler 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.WKUserContentControllerHostApi.addScriptMessageHandler was null, expected non-null int.'); + final int? arg_handlerIdentifier = (args[1] as int?); + assert(arg_handlerIdentifier != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler was null, expected non-null int.'); + final String? arg_name = (args[2] as String?); + assert(arg_name != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler was null, expected non-null String.'); + api.addScriptMessageHandler( + arg_identifier!, arg_handlerIdentifier!, arg_name!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeScriptMessageHandler', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeScriptMessageHandler 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.WKUserContentControllerHostApi.removeScriptMessageHandler was null, expected non-null int.'); + final String? arg_name = (args[1] as String?); + assert(arg_name != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeScriptMessageHandler was null, expected non-null String.'); + api.removeScriptMessageHandler(arg_identifier!, arg_name!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllScriptMessageHandlers', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllScriptMessageHandlers 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.WKUserContentControllerHostApi.removeAllScriptMessageHandlers was null, expected non-null int.'); + api.removeAllScriptMessageHandlers(arg_identifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.addUserScript', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addUserScript 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.WKUserContentControllerHostApi.addUserScript was null, expected non-null int.'); + final WKUserScriptData? arg_userScript = + (args[1] as WKUserScriptData?); + assert(arg_userScript != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addUserScript was null, expected non-null WKUserScriptData.'); + api.addUserScript(arg_identifier!, arg_userScript!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllUserScripts', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllUserScripts 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.WKUserContentControllerHostApi.removeAllUserScripts was null, expected non-null int.'); + api.removeAllUserScripts(arg_identifier!); + return {}; + }); + } + } + } +} + +class _TestWKPreferencesHostApiCodec extends StandardMessageCodec { + const _TestWKPreferencesHostApiCodec(); +} + +abstract class TestWKPreferencesHostApi { + static const MessageCodec codec = _TestWKPreferencesHostApiCodec(); + + void createFromWebViewConfiguration( + int identifier, int configurationIdentifier); + void setJavaScriptEnabled(int identifier, bool enabled); + static void setup(TestWKPreferencesHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKPreferencesHostApi.createFromWebViewConfiguration', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKPreferencesHostApi.createFromWebViewConfiguration 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.WKPreferencesHostApi.createFromWebViewConfiguration was null, expected non-null int.'); + final int? arg_configurationIdentifier = (args[1] as int?); + assert(arg_configurationIdentifier != null, + 'Argument for dev.flutter.pigeon.WKPreferencesHostApi.createFromWebViewConfiguration was null, expected non-null int.'); + api.createFromWebViewConfiguration( + arg_identifier!, arg_configurationIdentifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKPreferencesHostApi.setJavaScriptEnabled', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKPreferencesHostApi.setJavaScriptEnabled 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.WKPreferencesHostApi.setJavaScriptEnabled was null, expected non-null int.'); + final bool? arg_enabled = (args[1] as bool?); + assert(arg_enabled != null, + 'Argument for dev.flutter.pigeon.WKPreferencesHostApi.setJavaScriptEnabled was null, expected non-null bool.'); + api.setJavaScriptEnabled(arg_identifier!, arg_enabled!); + return {}; + }); + } + } + } +} + +class _TestWKScriptMessageHandlerHostApiCodec extends StandardMessageCodec { + const _TestWKScriptMessageHandlerHostApiCodec(); +} + +abstract class TestWKScriptMessageHandlerHostApi { + static const MessageCodec codec = + _TestWKScriptMessageHandlerHostApiCodec(); + + void create(int identifier); + static void setup(TestWKScriptMessageHandlerHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKScriptMessageHandlerHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKScriptMessageHandlerHostApi.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.WKScriptMessageHandlerHostApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return {}; + }); + } + } + } +} + +class _TestWKNavigationDelegateHostApiCodec extends StandardMessageCodec { + const _TestWKNavigationDelegateHostApiCodec(); +} + +abstract class TestWKNavigationDelegateHostApi { + static const MessageCodec codec = + _TestWKNavigationDelegateHostApiCodec(); + + void create(int identifier); + static void setup(TestWKNavigationDelegateHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKNavigationDelegateHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKNavigationDelegateHostApi.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.WKNavigationDelegateHostApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return {}; + }); + } + } + } +} + +class _TestNSObjectHostApiCodec extends StandardMessageCodec { + const _TestNSObjectHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSKeyValueObservingOptionsEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSKeyValueObservingOptionsEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestNSObjectHostApi { + static const MessageCodec codec = _TestNSObjectHostApiCodec(); + + void dispose(int identifier); + void addObserver(int identifier, int observerIdentifier, String keyPath, + List options); + void removeObserver(int identifier, int observerIdentifier, String keyPath); + static void setup(TestNSObjectHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectHostApi.dispose', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.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.NSObjectHostApi.dispose was null, expected non-null int.'); + api.dispose(arg_identifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectHostApi.addObserver', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.addObserver 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.NSObjectHostApi.addObserver was null, expected non-null int.'); + final int? arg_observerIdentifier = (args[1] as int?); + assert(arg_observerIdentifier != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.addObserver was null, expected non-null int.'); + final String? arg_keyPath = (args[2] as String?); + assert(arg_keyPath != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.addObserver was null, expected non-null String.'); + final List? arg_options = + (args[3] as List?) + ?.cast(); + assert(arg_options != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.addObserver was null, expected non-null List.'); + api.addObserver(arg_identifier!, arg_observerIdentifier!, + arg_keyPath!, arg_options!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NSObjectHostApi.removeObserver', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.removeObserver 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.NSObjectHostApi.removeObserver was null, expected non-null int.'); + final int? arg_observerIdentifier = (args[1] as int?); + assert(arg_observerIdentifier != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.removeObserver was null, expected non-null int.'); + final String? arg_keyPath = (args[2] as String?); + assert(arg_keyPath != null, + 'Argument for dev.flutter.pigeon.NSObjectHostApi.removeObserver was null, expected non-null String.'); + api.removeObserver( + arg_identifier!, arg_observerIdentifier!, arg_keyPath!); + return {}; + }); + } + } + } +} + +class _TestWKWebViewHostApiCodec extends StandardMessageCodec { + const _TestWKWebViewHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSErrorData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is NSHttpCookieData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is NSHttpCookiePropertyKeyEnumData) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is NSKeyValueChangeKeyEnumData) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is NSKeyValueObservingOptionsEnumData) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is NSUrlRequestData) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is WKAudiovisualMediaTypeEnumData) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is WKFrameInfoData) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else if (value is WKNavigationActionData) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is WKNavigationActionPolicyEnumData) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else if (value is WKScriptMessageData) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptData) { + buffer.putUint8(139); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptInjectionTimeEnumData) { + buffer.putUint8(140); + writeValue(buffer, value.encode()); + } else if (value is WKWebsiteDataTypeEnumData) { + buffer.putUint8(141); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSErrorData.decode(readValue(buffer)!); + + case 129: + return NSHttpCookieData.decode(readValue(buffer)!); + + case 130: + return NSHttpCookiePropertyKeyEnumData.decode(readValue(buffer)!); + + case 131: + return NSKeyValueChangeKeyEnumData.decode(readValue(buffer)!); + + case 132: + return NSKeyValueObservingOptionsEnumData.decode(readValue(buffer)!); + + case 133: + return NSUrlRequestData.decode(readValue(buffer)!); + + case 134: + return WKAudiovisualMediaTypeEnumData.decode(readValue(buffer)!); + + case 135: + return WKFrameInfoData.decode(readValue(buffer)!); + + case 136: + return WKNavigationActionData.decode(readValue(buffer)!); + + case 137: + return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); + + case 138: + return WKScriptMessageData.decode(readValue(buffer)!); + + case 139: + return WKUserScriptData.decode(readValue(buffer)!); + + case 140: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + + case 141: + return WKWebsiteDataTypeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestWKWebViewHostApi { + static const MessageCodec codec = _TestWKWebViewHostApiCodec(); + + void create(int identifier, int configurationIdentifier); + void setUIDelegate(int identifier, int? uiDelegateIdentifier); + void setNavigationDelegate(int identifier, int? navigationDelegateIdentifier); + String? getUrl(int identifier); + double getEstimatedProgress(int identifier); + void loadRequest(int identifier, NSUrlRequestData request); + void loadHtmlString(int identifier, String string, String? baseUrl); + void loadFileUrl(int identifier, String url, String readAccessUrl); + void loadFlutterAsset(int identifier, String key); + bool canGoBack(int identifier); + bool canGoForward(int identifier); + void goBack(int identifier); + void goForward(int identifier); + void reload(int identifier); + String? getTitle(int identifier); + void setAllowsBackForwardNavigationGestures(int identifier, bool allow); + void setCustomUserAgent(int identifier, String? userAgent); + Future evaluateJavaScript(int identifier, String javaScriptString); + static void setup(TestWKWebViewHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.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.WKWebViewHostApi.create was null, expected non-null int.'); + final int? arg_configurationIdentifier = (args[1] as int?); + assert(arg_configurationIdentifier != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.create was null, expected non-null int.'); + api.create(arg_identifier!, arg_configurationIdentifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setUIDelegate', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setUIDelegate 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.WKWebViewHostApi.setUIDelegate was null, expected non-null int.'); + final int? arg_uiDelegateIdentifier = (args[1] as int?); + api.setUIDelegate(arg_identifier!, arg_uiDelegateIdentifier); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setNavigationDelegate', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setNavigationDelegate 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.WKWebViewHostApi.setNavigationDelegate was null, expected non-null int.'); + final int? arg_navigationDelegateIdentifier = (args[1] as int?); + api.setNavigationDelegate( + arg_identifier!, arg_navigationDelegateIdentifier); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.getUrl', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.getUrl 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.WKWebViewHostApi.getUrl was null, expected non-null int.'); + final String? output = api.getUrl(arg_identifier!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.getEstimatedProgress', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.getEstimatedProgress 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.WKWebViewHostApi.getEstimatedProgress was null, expected non-null int.'); + final double output = api.getEstimatedProgress(arg_identifier!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadRequest', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadRequest 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.WKWebViewHostApi.loadRequest was null, expected non-null int.'); + final NSUrlRequestData? arg_request = (args[1] as NSUrlRequestData?); + assert(arg_request != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadRequest was null, expected non-null NSUrlRequestData.'); + api.loadRequest(arg_identifier!, arg_request!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadHtmlString', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadHtmlString 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.WKWebViewHostApi.loadHtmlString was null, expected non-null int.'); + final String? arg_string = (args[1] as String?); + assert(arg_string != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadHtmlString was null, expected non-null String.'); + final String? arg_baseUrl = (args[2] as String?); + api.loadHtmlString(arg_identifier!, arg_string!, arg_baseUrl); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl 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.WKWebViewHostApi.loadFileUrl was null, expected non-null int.'); + final String? arg_url = (args[1] as String?); + assert(arg_url != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl was null, expected non-null String.'); + final String? arg_readAccessUrl = (args[2] as String?); + assert(arg_readAccessUrl != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl was null, expected non-null String.'); + api.loadFileUrl(arg_identifier!, arg_url!, arg_readAccessUrl!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.loadFlutterAsset', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFlutterAsset 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.WKWebViewHostApi.loadFlutterAsset was null, expected non-null int.'); + final String? arg_key = (args[1] as String?); + assert(arg_key != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFlutterAsset was null, expected non-null String.'); + api.loadFlutterAsset(arg_identifier!, arg_key!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.canGoBack', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.canGoBack 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.WKWebViewHostApi.canGoBack was null, expected non-null int.'); + final bool output = api.canGoBack(arg_identifier!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.canGoForward', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.canGoForward 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.WKWebViewHostApi.canGoForward was null, expected non-null int.'); + final bool output = api.canGoForward(arg_identifier!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.goBack', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.goBack 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.WKWebViewHostApi.goBack was null, expected non-null int.'); + api.goBack(arg_identifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.goForward', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.goForward 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.WKWebViewHostApi.goForward was null, expected non-null int.'); + api.goForward(arg_identifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.reload', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.reload 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.WKWebViewHostApi.reload was null, expected non-null int.'); + api.reload(arg_identifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.getTitle', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.getTitle 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.WKWebViewHostApi.getTitle was null, expected non-null int.'); + final String? output = api.getTitle(arg_identifier!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setAllowsBackForwardNavigationGestures', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setAllowsBackForwardNavigationGestures 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.WKWebViewHostApi.setAllowsBackForwardNavigationGestures was null, expected non-null int.'); + final bool? arg_allow = (args[1] as bool?); + assert(arg_allow != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setAllowsBackForwardNavigationGestures was null, expected non-null bool.'); + api.setAllowsBackForwardNavigationGestures( + arg_identifier!, arg_allow!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.setCustomUserAgent', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setCustomUserAgent 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.WKWebViewHostApi.setCustomUserAgent was null, expected non-null int.'); + final String? arg_userAgent = (args[1] as String?); + api.setCustomUserAgent(arg_identifier!, arg_userAgent); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewHostApi.evaluateJavaScript', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.evaluateJavaScript 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.WKWebViewHostApi.evaluateJavaScript was null, expected non-null int.'); + final String? arg_javaScriptString = (args[1] as String?); + assert(arg_javaScriptString != null, + 'Argument for dev.flutter.pigeon.WKWebViewHostApi.evaluateJavaScript was null, expected non-null String.'); + final Object? output = await api.evaluateJavaScript( + arg_identifier!, arg_javaScriptString!); + return {'result': output}; + }); + } + } + } +} + +class _TestWKUIDelegateHostApiCodec extends StandardMessageCodec { + const _TestWKUIDelegateHostApiCodec(); +} + +abstract class TestWKUIDelegateHostApi { + static const MessageCodec codec = _TestWKUIDelegateHostApiCodec(); + + void create(int identifier); + static void setup(TestWKUIDelegateHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUIDelegateHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateHostApi.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.WKUIDelegateHostApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return {}; + }); + } + } + } +} + +class _TestWKHttpCookieStoreHostApiCodec extends StandardMessageCodec { + const _TestWKHttpCookieStoreHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NSHttpCookieData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is NSHttpCookiePropertyKeyEnumData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NSHttpCookieData.decode(readValue(buffer)!); + + case 129: + return NSHttpCookiePropertyKeyEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestWKHttpCookieStoreHostApi { + static const MessageCodec codec = + _TestWKHttpCookieStoreHostApiCodec(); + + void createFromWebsiteDataStore( + int identifier, int websiteDataStoreIdentifier); + Future setCookie(int identifier, NSHttpCookieData cookie); + static void setup(TestWKHttpCookieStoreHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKHttpCookieStoreHostApi.createFromWebsiteDataStore', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKHttpCookieStoreHostApi.createFromWebsiteDataStore 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.WKHttpCookieStoreHostApi.createFromWebsiteDataStore was null, expected non-null int.'); + final int? arg_websiteDataStoreIdentifier = (args[1] as int?); + assert(arg_websiteDataStoreIdentifier != null, + 'Argument for dev.flutter.pigeon.WKHttpCookieStoreHostApi.createFromWebsiteDataStore was null, expected non-null int.'); + api.createFromWebsiteDataStore( + arg_identifier!, arg_websiteDataStoreIdentifier!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKHttpCookieStoreHostApi.setCookie', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKHttpCookieStoreHostApi.setCookie 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.WKHttpCookieStoreHostApi.setCookie was null, expected non-null int.'); + final NSHttpCookieData? arg_cookie = (args[1] as NSHttpCookieData?); + assert(arg_cookie != null, + 'Argument for dev.flutter.pigeon.WKHttpCookieStoreHostApi.setCookie was null, expected non-null NSHttpCookieData.'); + await api.setCookie(arg_identifier!, arg_cookie!); + return {}; + }); + } + } + } +} 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 new file mode 100644 index 000000000000..87b659885b52 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart @@ -0,0 +1,170 @@ +// 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_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/foundation/foundation.dart'; +import 'package:webview_flutter_wkwebview/src/foundation/foundation_api_impls.dart'; + +import '../common/test_web_kit.pigeon.dart'; +import 'foundation_test.mocks.dart'; + +@GenerateMocks([ + TestNSObjectHostApi, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Foundation', () { + late InstanceManager instanceManager; + + setUp(() { + instanceManager = InstanceManager(onWeakReferenceRemoved: (_) {}); + }); + + group('NSObject', () { + late MockTestNSObjectHostApi mockPlatformHostApi; + + late NSObject object; + + setUp(() { + mockPlatformHostApi = MockTestNSObjectHostApi(); + TestNSObjectHostApi.setup(mockPlatformHostApi); + + object = NSObject.detached(instanceManager: instanceManager); + instanceManager.addDartCreatedInstance(object); + }); + + tearDown(() { + TestNSObjectHostApi.setup(null); + }); + + test('addObserver', () async { + final NSObject observer = NSObject.detached( + instanceManager: instanceManager, + ); + instanceManager.addDartCreatedInstance(observer); + + await object.addObserver( + observer, + keyPath: 'aKeyPath', + options: { + NSKeyValueObservingOptions.initialValue, + NSKeyValueObservingOptions.priorNotification, + }, + ); + + final List optionsData = + verify(mockPlatformHostApi.addObserver( + instanceManager.getIdentifier(object), + instanceManager.getIdentifier(observer), + 'aKeyPath', + captureAny, + )).captured.single as List; + + expect(optionsData, hasLength(2)); + expect( + optionsData[0]!.value, + NSKeyValueObservingOptionsEnum.initialValue, + ); + expect( + optionsData[1]!.value, + NSKeyValueObservingOptionsEnum.priorNotification, + ); + }); + + test('removeObserver', () async { + final NSObject observer = NSObject.detached( + instanceManager: instanceManager, + ); + instanceManager.addDartCreatedInstance(observer); + + await object.removeObserver(observer, keyPath: 'aKeyPath'); + + verify(mockPlatformHostApi.removeObserver( + instanceManager.getIdentifier(object), + instanceManager.getIdentifier(observer), + 'aKeyPath', + )); + }); + + test('NSObjectHostApi.dispose', () async { + int? callbackIdentifier; + final InstanceManager instanceManager = + InstanceManager(onWeakReferenceRemoved: (int identifier) { + callbackIdentifier = identifier; + }); + + final NSObject object = NSObject.detached( + instanceManager: instanceManager, + ); + final int identifier = instanceManager.addDartCreatedInstance(object); + + NSObject.dispose(object); + expect(callbackIdentifier, identifier); + }); + + test('observeValue', () async { + final Completer> argsCompleter = + Completer>(); + + FoundationFlutterApis.instance = FoundationFlutterApis( + instanceManager: instanceManager, + ); + + object = NSObject.detached( + instanceManager: instanceManager, + observeValue: ( + String keyPath, + NSObject object, + Map change, + ) { + argsCompleter.complete([keyPath, object, change]); + }, + ); + instanceManager.addHostCreatedInstance(object, 1); + + FoundationFlutterApis.instance.object.observeValue( + 1, + 'keyPath', + 1, + [ + NSKeyValueChangeKeyEnumData(value: NSKeyValueChangeKeyEnum.oldValue) + ], + ['value'], + ); + + expect( + argsCompleter.future, + completion([ + 'keyPath', + object, + { + NSKeyValueChangeKey.oldValue: 'value', + }, + ]), + ); + }); + + test('NSObjectFlutterApi.dispose', () { + FoundationFlutterApis.instance = FoundationFlutterApis( + instanceManager: instanceManager, + ); + + object = NSObject.detached(instanceManager: instanceManager); + instanceManager.addHostCreatedInstance(object, 1); + + instanceManager.removeWeakReference(object); + FoundationFlutterApis.instance.object.dispose(1); + + expect(instanceManager.containsIdentifier(1), isFalse); + }); + }); + }); +} 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 new file mode 100644 index 000000000000..62a51e17bc75 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart @@ -0,0 +1,48 @@ +// Mocks generated by Mockito 5.2.0 from annotations +// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart. +// Do not manually edit this file. + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart' + as _i3; + +import '../common/test_web_kit.pigeon.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 + +/// A class which mocks [TestNSObjectHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestNSObjectHostApi extends _i1.Mock + implements _i2.TestNSObjectHostApi { + MockTestNSObjectHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void dispose(int? identifier) => + super.noSuchMethod(Invocation.method(#dispose, [identifier]), + returnValueForMissingStub: null); + @override + void addObserver(int? identifier, int? observerIdentifier, String? keyPath, + List<_i3.NSKeyValueObservingOptionsEnumData?>? options) => + super.noSuchMethod( + Invocation.method( + #addObserver, [identifier, observerIdentifier, keyPath, options]), + returnValueForMissingStub: null); + @override + void removeObserver( + int? identifier, int? observerIdentifier, String? keyPath) => + super.noSuchMethod( + Invocation.method( + #removeObserver, [identifier, observerIdentifier, keyPath]), + returnValueForMissingStub: null); +} 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 new file mode 100644 index 000000000000..f2250e1ac423 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart @@ -0,0 +1,122 @@ +// 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:math'; + +import 'package:flutter/material.dart'; +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/ui_kit/ui_kit.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; + +import '../common/test_web_kit.pigeon.dart'; +import 'ui_kit_test.mocks.dart'; + +@GenerateMocks([ + TestWKWebViewConfigurationHostApi, + TestWKWebViewHostApi, + TestUIScrollViewHostApi, + TestUIViewHostApi, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('UIKit', () { + late InstanceManager instanceManager; + + setUp(() { + instanceManager = InstanceManager(onWeakReferenceRemoved: (_) {}); + }); + + group('UIScrollView', () { + late MockTestUIScrollViewHostApi mockPlatformHostApi; + + late UIScrollView scrollView; + late int scrollViewInstanceId; + + setUp(() { + mockPlatformHostApi = MockTestUIScrollViewHostApi(); + TestUIScrollViewHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi(), + ); + TestWKWebViewHostApi.setup(MockTestWKWebViewHostApi()); + final WKWebView webView = WKWebView( + WKWebViewConfiguration(instanceManager: instanceManager), + instanceManager: instanceManager, + ); + + scrollView = UIScrollView.fromWebView( + webView, + instanceManager: instanceManager, + ); + scrollViewInstanceId = instanceManager.getIdentifier(scrollView)!; + }); + + tearDown(() { + TestUIScrollViewHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + TestWKWebViewHostApi.setup(null); + }); + + test('getContentOffset', () async { + when(mockPlatformHostApi.getContentOffset(scrollViewInstanceId)) + .thenReturn([4.0, 10.0]); + expect( + scrollView.getContentOffset(), + completion(const Point(4.0, 10.0)), + ); + }); + + test('scrollBy', () async { + await scrollView.scrollBy(const Point(4.0, 10.0)); + verify(mockPlatformHostApi.scrollBy(scrollViewInstanceId, 4.0, 10.0)); + }); + + test('setContentOffset', () async { + await scrollView.setContentOffset(const Point(4.0, 10.0)); + verify(mockPlatformHostApi.setContentOffset( + scrollViewInstanceId, + 4.0, + 10.0, + )); + }); + }); + + group('UIView', () { + late MockTestUIViewHostApi mockPlatformHostApi; + + late UIView view; + late int viewInstanceId; + + setUp(() { + mockPlatformHostApi = MockTestUIViewHostApi(); + TestUIViewHostApi.setup(mockPlatformHostApi); + + view = UIView.detached(instanceManager: instanceManager); + viewInstanceId = instanceManager.addDartCreatedInstance(view); + }); + + tearDown(() { + TestUIViewHostApi.setup(null); + }); + + test('setBackgroundColor', () async { + await view.setBackgroundColor(Colors.red); + verify(mockPlatformHostApi.setBackgroundColor( + viewInstanceId, + Colors.red.value, + )); + }); + + test('setOpaque', () async { + await view.setOpaque(false); + verify(mockPlatformHostApi.setOpaque(viewInstanceId, false)); + }); + }); + }); +} 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 new file mode 100644 index 000000000000..a382ecff677c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart @@ -0,0 +1,196 @@ +// Mocks generated by Mockito 5.2.0 from annotations +// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart. +// Do not manually edit this file. + +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 '../common/test_web_kit.pigeon.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 + +/// A class which mocks [TestWKWebViewConfigurationHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKWebViewConfigurationHostApi extends _i1.Mock + implements _i2.TestWKWebViewConfigurationHostApi { + MockTestWKWebViewConfigurationHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? identifier) => + super.noSuchMethod(Invocation.method(#create, [identifier]), + returnValueForMissingStub: null); + @override + void createFromWebView(int? identifier, int? webViewIdentifier) => + super.noSuchMethod( + Invocation.method( + #createFromWebView, [identifier, webViewIdentifier]), + returnValueForMissingStub: null); + @override + void setAllowsInlineMediaPlayback(int? identifier, bool? allow) => + super.noSuchMethod( + Invocation.method(#setAllowsInlineMediaPlayback, [identifier, allow]), + returnValueForMissingStub: null); + @override + void setMediaTypesRequiringUserActionForPlayback( + int? identifier, List<_i3.WKAudiovisualMediaTypeEnumData?>? types) => + super.noSuchMethod( + Invocation.method(#setMediaTypesRequiringUserActionForPlayback, + [identifier, types]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKWebViewHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKWebViewHostApi extends _i1.Mock + implements _i2.TestWKWebViewHostApi { + MockTestWKWebViewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? identifier, int? configurationIdentifier) => + super.noSuchMethod( + Invocation.method(#create, [identifier, configurationIdentifier]), + returnValueForMissingStub: null); + @override + void setUIDelegate(int? identifier, int? uiDelegateIdentifier) => + super.noSuchMethod( + Invocation.method(#setUIDelegate, [identifier, uiDelegateIdentifier]), + returnValueForMissingStub: null); + @override + void setNavigationDelegate( + int? identifier, int? navigationDelegateIdentifier) => + super.noSuchMethod( + Invocation.method(#setNavigationDelegate, + [identifier, navigationDelegateIdentifier]), + returnValueForMissingStub: null); + @override + String? getUrl(int? identifier) => + (super.noSuchMethod(Invocation.method(#getUrl, [identifier])) as String?); + @override + double getEstimatedProgress(int? identifier) => (super.noSuchMethod( + Invocation.method(#getEstimatedProgress, [identifier]), + returnValue: 0.0) as double); + @override + void loadRequest(int? identifier, _i3.NSUrlRequestData? request) => + super.noSuchMethod(Invocation.method(#loadRequest, [identifier, request]), + returnValueForMissingStub: null); + @override + void loadHtmlString(int? identifier, String? string, String? baseUrl) => + super.noSuchMethod( + Invocation.method(#loadHtmlString, [identifier, string, baseUrl]), + returnValueForMissingStub: null); + @override + void loadFileUrl(int? identifier, String? url, String? readAccessUrl) => + super.noSuchMethod( + Invocation.method(#loadFileUrl, [identifier, url, readAccessUrl]), + returnValueForMissingStub: null); + @override + void loadFlutterAsset(int? identifier, String? key) => super.noSuchMethod( + Invocation.method(#loadFlutterAsset, [identifier, key]), + returnValueForMissingStub: null); + @override + bool canGoBack(int? identifier) => + (super.noSuchMethod(Invocation.method(#canGoBack, [identifier]), + returnValue: false) as bool); + @override + bool canGoForward(int? identifier) => + (super.noSuchMethod(Invocation.method(#canGoForward, [identifier]), + returnValue: false) as bool); + @override + void goBack(int? identifier) => + super.noSuchMethod(Invocation.method(#goBack, [identifier]), + returnValueForMissingStub: null); + @override + void goForward(int? identifier) => + super.noSuchMethod(Invocation.method(#goForward, [identifier]), + returnValueForMissingStub: null); + @override + void reload(int? identifier) => + super.noSuchMethod(Invocation.method(#reload, [identifier]), + returnValueForMissingStub: null); + @override + String? getTitle(int? identifier) => + (super.noSuchMethod(Invocation.method(#getTitle, [identifier])) + as String?); + @override + void setAllowsBackForwardNavigationGestures(int? identifier, bool? allow) => + super.noSuchMethod( + Invocation.method( + #setAllowsBackForwardNavigationGestures, [identifier, allow]), + returnValueForMissingStub: null); + @override + void setCustomUserAgent(int? identifier, String? userAgent) => + super.noSuchMethod( + Invocation.method(#setCustomUserAgent, [identifier, userAgent]), + returnValueForMissingStub: null); + @override + _i4.Future evaluateJavaScript( + int? identifier, String? javaScriptString) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavaScript, [identifier, javaScriptString]), + returnValue: Future.value()) as _i4.Future); +} + +/// A class which mocks [TestUIScrollViewHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestUIScrollViewHostApi extends _i1.Mock + implements _i2.TestUIScrollViewHostApi { + MockTestUIScrollViewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void createFromWebView(int? identifier, int? webViewIdentifier) => + super.noSuchMethod( + Invocation.method( + #createFromWebView, [identifier, webViewIdentifier]), + returnValueForMissingStub: null); + @override + List getContentOffset(int? identifier) => + (super.noSuchMethod(Invocation.method(#getContentOffset, [identifier]), + returnValue: []) as List); + @override + void scrollBy(int? identifier, double? x, double? y) => + super.noSuchMethod(Invocation.method(#scrollBy, [identifier, x, y]), + returnValueForMissingStub: null); + @override + void setContentOffset(int? identifier, double? x, double? y) => super + .noSuchMethod(Invocation.method(#setContentOffset, [identifier, x, y]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestUIViewHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestUIViewHostApi extends _i1.Mock implements _i2.TestUIViewHostApi { + MockTestUIViewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void setBackgroundColor(int? identifier, int? value) => super.noSuchMethod( + Invocation.method(#setBackgroundColor, [identifier, value]), + returnValueForMissingStub: null); + @override + void setOpaque(int? identifier, bool? opaque) => + super.noSuchMethod(Invocation.method(#setOpaque, [identifier, opaque]), + returnValueForMissingStub: null); +} 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 new file mode 100644 index 000000000000..4000e0d718da --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart @@ -0,0 +1,939 @@ +// 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_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/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 'web_kit_test.mocks.dart'; + +@GenerateMocks([ + TestWKHttpCookieStoreHostApi, + TestWKNavigationDelegateHostApi, + TestWKPreferencesHostApi, + TestWKScriptMessageHandlerHostApi, + TestWKUIDelegateHostApi, + TestWKUserContentControllerHostApi, + TestWKWebViewConfigurationHostApi, + TestWKWebViewHostApi, + TestWKWebsiteDataStoreHostApi, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('WebKit', () { + late InstanceManager instanceManager; + late WebKitFlutterApis flutterApis; + + setUp(() { + instanceManager = InstanceManager(onWeakReferenceRemoved: (_) {}); + flutterApis = WebKitFlutterApis(instanceManager: instanceManager); + WebKitFlutterApis.instance = flutterApis; + }); + + group('WKWebsiteDataStore', () { + late MockTestWKWebsiteDataStoreHostApi mockPlatformHostApi; + + late WKWebsiteDataStore websiteDataStore; + + late WKWebViewConfiguration webViewConfiguration; + + setUp(() { + mockPlatformHostApi = MockTestWKWebsiteDataStoreHostApi(); + TestWKWebsiteDataStoreHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi(), + ); + webViewConfiguration = WKWebViewConfiguration( + instanceManager: instanceManager, + ); + + websiteDataStore = WKWebsiteDataStore.fromWebViewConfiguration( + webViewConfiguration, + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKWebsiteDataStoreHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + }); + + test('WKWebViewConfigurationFlutterApi.create', () { + final WebKitFlutterApis flutterApis = WebKitFlutterApis( + instanceManager: instanceManager, + ); + + flutterApis.webViewConfiguration.create(2); + + expect(instanceManager.containsIdentifier(2), isTrue); + expect( + instanceManager.getInstanceWithWeakReference(2), + isA(), + ); + }); + + test('createFromWebViewConfiguration', () { + verify(mockPlatformHostApi.createFromWebViewConfiguration( + instanceManager.getIdentifier(websiteDataStore), + instanceManager.getIdentifier(webViewConfiguration), + )); + }); + + test('createDefaultDataStore', () { + final WKWebsiteDataStore defaultDataStore = + WKWebsiteDataStore.defaultDataStore; + verify( + mockPlatformHostApi.createDefaultDataStore( + NSObject.globalInstanceManager.getIdentifier(defaultDataStore), + ), + ); + }); + + test('removeDataOfTypes', () { + when(mockPlatformHostApi.removeDataOfTypes( + any, + any, + any, + )).thenAnswer((_) => Future.value(true)); + + expect( + websiteDataStore.removeDataOfTypes( + {WKWebsiteDataType.cookies}, + DateTime.fromMillisecondsSinceEpoch(5000), + ), + completion(true), + ); + + final List typeData = + verify(mockPlatformHostApi.removeDataOfTypes( + instanceManager.getIdentifier(websiteDataStore), + captureAny, + 5.0, + )).captured.single.cast() + as List; + + expect(typeData.single.value, WKWebsiteDataTypeEnum.cookies); + }); + }); + + group('WKHttpCookieStore', () { + late MockTestWKHttpCookieStoreHostApi mockPlatformHostApi; + + late WKHttpCookieStore httpCookieStore; + + late WKWebsiteDataStore websiteDataStore; + + setUp(() { + mockPlatformHostApi = MockTestWKHttpCookieStoreHostApi(); + TestWKHttpCookieStoreHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi(), + ); + TestWKWebsiteDataStoreHostApi.setup( + MockTestWKWebsiteDataStoreHostApi(), + ); + + websiteDataStore = WKWebsiteDataStore.fromWebViewConfiguration( + WKWebViewConfiguration(instanceManager: instanceManager), + instanceManager: instanceManager, + ); + + httpCookieStore = WKHttpCookieStore.fromWebsiteDataStore( + websiteDataStore, + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKHttpCookieStoreHostApi.setup(null); + TestWKWebsiteDataStoreHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + }); + + test('createFromWebsiteDataStore', () { + verify(mockPlatformHostApi.createFromWebsiteDataStore( + instanceManager.getIdentifier(httpCookieStore), + instanceManager.getIdentifier(websiteDataStore), + )); + }); + + test('setCookie', () async { + await httpCookieStore.setCookie( + const NSHttpCookie.withProperties({ + NSHttpCookiePropertyKey.comment: 'aComment', + })); + + final NSHttpCookieData cookie = verify( + mockPlatformHostApi.setCookie( + instanceManager.getIdentifier(httpCookieStore), + captureAny, + ), + ).captured.single as NSHttpCookieData; + + expect( + cookie.propertyKeys.single!.value, + NSHttpCookiePropertyKeyEnum.comment, + ); + expect(cookie.propertyValues.single, 'aComment'); + }); + }); + + group('WKScriptMessageHandler', () { + late MockTestWKScriptMessageHandlerHostApi mockPlatformHostApi; + + late WKScriptMessageHandler scriptMessageHandler; + + setUp(() async { + mockPlatformHostApi = MockTestWKScriptMessageHandlerHostApi(); + TestWKScriptMessageHandlerHostApi.setup(mockPlatformHostApi); + + scriptMessageHandler = WKScriptMessageHandler( + didReceiveScriptMessage: (_, __) {}, + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKScriptMessageHandlerHostApi.setup(null); + }); + + test('create', () async { + verify(mockPlatformHostApi.create( + instanceManager.getIdentifier(scriptMessageHandler), + )); + }); + + test('didReceiveScriptMessage', () async { + final Completer> argsCompleter = + Completer>(); + + WebKitFlutterApis.instance = WebKitFlutterApis( + instanceManager: instanceManager, + ); + + scriptMessageHandler = WKScriptMessageHandler( + instanceManager: instanceManager, + didReceiveScriptMessage: ( + WKUserContentController userContentController, + WKScriptMessage message, + ) { + argsCompleter.complete([userContentController, message]); + }, + ); + + final WKUserContentController userContentController = + WKUserContentController.detached( + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance(userContentController, 2); + + WebKitFlutterApis.instance.scriptMessageHandler.didReceiveScriptMessage( + instanceManager.getIdentifier(scriptMessageHandler)!, + 2, + WKScriptMessageData(name: 'name'), + ); + + expect( + argsCompleter.future, + completion([userContentController, isA()]), + ); + }); + }); + + group('WKPreferences', () { + late MockTestWKPreferencesHostApi mockPlatformHostApi; + + late WKPreferences preferences; + + late WKWebViewConfiguration webViewConfiguration; + + setUp(() { + mockPlatformHostApi = MockTestWKPreferencesHostApi(); + TestWKPreferencesHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi(), + ); + webViewConfiguration = WKWebViewConfiguration( + instanceManager: instanceManager, + ); + + preferences = WKPreferences.fromWebViewConfiguration( + webViewConfiguration, + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKPreferencesHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + }); + + test('createFromWebViewConfiguration', () async { + verify(mockPlatformHostApi.createFromWebViewConfiguration( + instanceManager.getIdentifier(preferences), + instanceManager.getIdentifier(webViewConfiguration), + )); + }); + + test('setJavaScriptEnabled', () async { + await preferences.setJavaScriptEnabled(true); + verify(mockPlatformHostApi.setJavaScriptEnabled( + instanceManager.getIdentifier(preferences), + true, + )); + }); + }); + + group('WKUserContentController', () { + late MockTestWKUserContentControllerHostApi mockPlatformHostApi; + + late WKUserContentController userContentController; + + late WKWebViewConfiguration webViewConfiguration; + + setUp(() { + mockPlatformHostApi = MockTestWKUserContentControllerHostApi(); + TestWKUserContentControllerHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi(), + ); + webViewConfiguration = WKWebViewConfiguration( + instanceManager: instanceManager, + ); + + userContentController = + WKUserContentController.fromWebViewConfiguration( + webViewConfiguration, + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKUserContentControllerHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + }); + + test('createFromWebViewConfiguration', () async { + verify(mockPlatformHostApi.createFromWebViewConfiguration( + instanceManager.getIdentifier(userContentController), + instanceManager.getIdentifier(webViewConfiguration), + )); + }); + + test('addScriptMessageHandler', () async { + TestWKScriptMessageHandlerHostApi.setup( + MockTestWKScriptMessageHandlerHostApi(), + ); + final WKScriptMessageHandler handler = WKScriptMessageHandler( + didReceiveScriptMessage: (_, __) {}, + instanceManager: instanceManager, + ); + + userContentController.addScriptMessageHandler(handler, 'handlerName'); + verify(mockPlatformHostApi.addScriptMessageHandler( + instanceManager.getIdentifier(userContentController), + instanceManager.getIdentifier(handler), + 'handlerName', + )); + }); + + test('removeScriptMessageHandler', () async { + userContentController.removeScriptMessageHandler('handlerName'); + verify(mockPlatformHostApi.removeScriptMessageHandler( + instanceManager.getIdentifier(userContentController), + 'handlerName', + )); + }); + + test('removeAllScriptMessageHandlers', () async { + userContentController.removeAllScriptMessageHandlers(); + verify(mockPlatformHostApi.removeAllScriptMessageHandlers( + instanceManager.getIdentifier(userContentController), + )); + }); + + test('addUserScript', () { + userContentController.addUserScript(const WKUserScript( + 'aScript', + WKUserScriptInjectionTime.atDocumentEnd, + isMainFrameOnly: false, + )); + verify(mockPlatformHostApi.addUserScript( + instanceManager.getIdentifier(userContentController), + argThat(isA()), + )); + }); + + test('removeAllUserScripts', () { + userContentController.removeAllUserScripts(); + verify(mockPlatformHostApi.removeAllUserScripts( + instanceManager.getIdentifier(userContentController), + )); + }); + }); + + group('WKWebViewConfiguration', () { + late MockTestWKWebViewConfigurationHostApi mockPlatformHostApi; + + late WKWebViewConfiguration webViewConfiguration; + + setUp(() async { + mockPlatformHostApi = MockTestWKWebViewConfigurationHostApi(); + TestWKWebViewConfigurationHostApi.setup(mockPlatformHostApi); + + webViewConfiguration = WKWebViewConfiguration( + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKWebViewConfigurationHostApi.setup(null); + }); + + test('create', () async { + verify( + mockPlatformHostApi.create(instanceManager.getIdentifier( + webViewConfiguration, + )), + ); + }); + + test('createFromWebView', () async { + TestWKWebViewHostApi.setup(MockTestWKWebViewHostApi()); + final WKWebView webView = WKWebView( + webViewConfiguration, + instanceManager: instanceManager, + ); + + final WKWebViewConfiguration configurationFromWebView = + WKWebViewConfiguration.fromWebView( + webView, + instanceManager: instanceManager, + ); + verify(mockPlatformHostApi.createFromWebView( + instanceManager.getIdentifier(configurationFromWebView), + instanceManager.getIdentifier(webView), + )); + }); + + test('allowsInlineMediaPlayback', () { + webViewConfiguration.setAllowsInlineMediaPlayback(true); + verify(mockPlatformHostApi.setAllowsInlineMediaPlayback( + instanceManager.getIdentifier(webViewConfiguration), + true, + )); + }); + + test('mediaTypesRequiringUserActionForPlayback', () { + webViewConfiguration.setMediaTypesRequiringUserActionForPlayback( + { + WKAudiovisualMediaType.audio, + WKAudiovisualMediaType.video, + }, + ); + + final List typeData = verify( + mockPlatformHostApi.setMediaTypesRequiringUserActionForPlayback( + instanceManager.getIdentifier(webViewConfiguration), + captureAny, + )).captured.single as List; + + expect(typeData, hasLength(2)); + expect(typeData[0]!.value, WKAudiovisualMediaTypeEnum.audio); + expect(typeData[1]!.value, WKAudiovisualMediaTypeEnum.video); + }); + }); + + group('WKNavigationDelegate', () { + late MockTestWKNavigationDelegateHostApi mockPlatformHostApi; + + late WKWebView webView; + + late WKNavigationDelegate navigationDelegate; + + setUp(() async { + mockPlatformHostApi = MockTestWKNavigationDelegateHostApi(); + TestWKNavigationDelegateHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi(), + ); + TestWKWebViewHostApi.setup(MockTestWKWebViewHostApi()); + webView = WKWebView( + WKWebViewConfiguration(instanceManager: instanceManager), + instanceManager: instanceManager, + ); + + navigationDelegate = WKNavigationDelegate( + instanceManager: instanceManager, + ); + }); + + tearDown(() { + TestWKNavigationDelegateHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + TestWKWebViewHostApi.setup(null); + }); + + test('create', () async { + navigationDelegate = WKNavigationDelegate( + instanceManager: instanceManager, + ); + + verify(mockPlatformHostApi.create( + instanceManager.getIdentifier(navigationDelegate), + )); + }); + + test('didFinishNavigation', () async { + final Completer> argsCompleter = + Completer>(); + + WebKitFlutterApis.instance = WebKitFlutterApis( + instanceManager: instanceManager, + ); + + navigationDelegate = WKNavigationDelegate( + instanceManager: instanceManager, + didFinishNavigation: (WKWebView webView, String? url) { + argsCompleter.complete([webView, url]); + }, + ); + + WebKitFlutterApis.instance.navigationDelegate.didFinishNavigation( + instanceManager.getIdentifier(navigationDelegate)!, + instanceManager.getIdentifier(webView)!, + 'url', + ); + + expect(argsCompleter.future, completion([webView, 'url'])); + }); + + test('didStartProvisionalNavigation', () async { + final Completer> argsCompleter = + Completer>(); + + WebKitFlutterApis.instance = WebKitFlutterApis( + instanceManager: instanceManager, + ); + + navigationDelegate = WKNavigationDelegate( + instanceManager: instanceManager, + didStartProvisionalNavigation: (WKWebView webView, String? url) { + argsCompleter.complete([webView, url]); + }, + ); + + WebKitFlutterApis.instance.navigationDelegate + .didStartProvisionalNavigation( + instanceManager.getIdentifier(navigationDelegate)!, + instanceManager.getIdentifier(webView)!, + 'url', + ); + + expect(argsCompleter.future, completion([webView, 'url'])); + }); + + test('decidePolicyForNavigationAction', () async { + WebKitFlutterApis.instance = WebKitFlutterApis( + instanceManager: instanceManager, + ); + + navigationDelegate = WKNavigationDelegate( + instanceManager: instanceManager, + decidePolicyForNavigationAction: ( + WKWebView webView, + WKNavigationAction navigationAction, + ) async { + return WKNavigationActionPolicy.cancel; + }, + ); + + final WKNavigationActionPolicyEnumData policyData = + await WebKitFlutterApis.instance.navigationDelegate + .decidePolicyForNavigationAction( + instanceManager.getIdentifier(navigationDelegate)!, + instanceManager.getIdentifier(webView)!, + WKNavigationActionData( + request: NSUrlRequestData( + url: 'url', + allHttpHeaderFields: {}, + ), + targetFrame: WKFrameInfoData(isMainFrame: false), + ), + ); + + expect(policyData.value, WKNavigationActionPolicyEnum.cancel); + }); + + test('didFailNavigation', () async { + final Completer> argsCompleter = + Completer>(); + + WebKitFlutterApis.instance = WebKitFlutterApis( + instanceManager: instanceManager, + ); + + navigationDelegate = WKNavigationDelegate( + instanceManager: instanceManager, + didFailNavigation: (WKWebView webView, NSError error) { + argsCompleter.complete([webView, error]); + }, + ); + + WebKitFlutterApis.instance.navigationDelegate.didFailNavigation( + instanceManager.getIdentifier(navigationDelegate)!, + instanceManager.getIdentifier(webView)!, + NSErrorData( + code: 23, + domain: 'Hello', + localizedDescription: 'localiziedDescription', + ), + ); + + expect( + argsCompleter.future, + completion([webView, isA()]), + ); + }); + + test('didFailProvisionalNavigation', () async { + final Completer> argsCompleter = + Completer>(); + + WebKitFlutterApis.instance = WebKitFlutterApis( + instanceManager: instanceManager, + ); + + navigationDelegate = WKNavigationDelegate( + instanceManager: instanceManager, + didFailProvisionalNavigation: (WKWebView webView, NSError error) { + argsCompleter.complete([webView, error]); + }, + ); + + WebKitFlutterApis.instance.navigationDelegate + .didFailProvisionalNavigation( + instanceManager.getIdentifier(navigationDelegate)!, + instanceManager.getIdentifier(webView)!, + NSErrorData( + code: 23, + domain: 'Hello', + localizedDescription: 'localiziedDescription', + ), + ); + + expect( + argsCompleter.future, + completion([webView, isA()]), + ); + }); + + test('webViewWebContentProcessDidTerminate', () async { + final Completer> argsCompleter = + Completer>(); + + WebKitFlutterApis.instance = WebKitFlutterApis( + instanceManager: instanceManager, + ); + + navigationDelegate = WKNavigationDelegate( + instanceManager: instanceManager, + webViewWebContentProcessDidTerminate: (WKWebView webView) { + argsCompleter.complete([webView]); + }, + ); + + WebKitFlutterApis.instance.navigationDelegate + .webViewWebContentProcessDidTerminate( + instanceManager.getIdentifier(navigationDelegate)!, + instanceManager.getIdentifier(webView)!, + ); + + expect(argsCompleter.future, completion([webView])); + }); + }); + + group('WKWebView', () { + late MockTestWKWebViewHostApi mockPlatformHostApi; + + late WKWebViewConfiguration webViewConfiguration; + + late WKWebView webView; + late int webViewInstanceId; + + setUp(() { + mockPlatformHostApi = MockTestWKWebViewHostApi(); + TestWKWebViewHostApi.setup(mockPlatformHostApi); + + TestWKWebViewConfigurationHostApi.setup( + MockTestWKWebViewConfigurationHostApi()); + webViewConfiguration = WKWebViewConfiguration( + instanceManager: instanceManager, + ); + + webView = WKWebView( + webViewConfiguration, + instanceManager: instanceManager, + ); + webViewInstanceId = instanceManager.getIdentifier(webView)!; + }); + + tearDown(() { + TestWKWebViewHostApi.setup(null); + TestWKWebViewConfigurationHostApi.setup(null); + }); + + test('create', () async { + verify(mockPlatformHostApi.create( + instanceManager.getIdentifier(webView), + instanceManager.getIdentifier( + webViewConfiguration, + ), + )); + }); + + test('setUIDelegate', () async { + TestWKUIDelegateHostApi.setup(MockTestWKUIDelegateHostApi()); + final WKUIDelegate uiDelegate = WKUIDelegate( + instanceManager: instanceManager, + ); + + await webView.setUIDelegate(uiDelegate); + verify(mockPlatformHostApi.setUIDelegate( + webViewInstanceId, + instanceManager.getIdentifier(uiDelegate), + )); + + TestWKUIDelegateHostApi.setup(null); + }); + + test('setNavigationDelegate', () async { + TestWKNavigationDelegateHostApi.setup( + MockTestWKNavigationDelegateHostApi(), + ); + final WKNavigationDelegate navigationDelegate = WKNavigationDelegate( + instanceManager: instanceManager, + ); + + await webView.setNavigationDelegate(navigationDelegate); + verify(mockPlatformHostApi.setNavigationDelegate( + webViewInstanceId, + instanceManager.getIdentifier(navigationDelegate), + )); + + TestWKNavigationDelegateHostApi.setup(null); + }); + + test('getUrl', () { + when( + mockPlatformHostApi.getUrl(webViewInstanceId), + ).thenReturn('www.flutter.dev'); + expect(webView.getUrl(), completion('www.flutter.dev')); + }); + + test('getEstimatedProgress', () { + when( + mockPlatformHostApi.getEstimatedProgress(webViewInstanceId), + ).thenReturn(54.5); + expect(webView.getEstimatedProgress(), completion(54.5)); + }); + + test('loadRequest', () { + webView.loadRequest(const NSUrlRequest(url: 'www.flutter.dev')); + verify(mockPlatformHostApi.loadRequest( + webViewInstanceId, + argThat(isA()), + )); + }); + + test('loadHtmlString', () { + webView.loadHtmlString('a', baseUrl: 'b'); + verify(mockPlatformHostApi.loadHtmlString(webViewInstanceId, 'a', 'b')); + }); + + test('loadFileUrl', () { + webView.loadFileUrl('a', readAccessUrl: 'b'); + verify(mockPlatformHostApi.loadFileUrl(webViewInstanceId, 'a', 'b')); + }); + + test('loadFlutterAsset', () { + webView.loadFlutterAsset('a'); + verify(mockPlatformHostApi.loadFlutterAsset(webViewInstanceId, 'a')); + }); + + test('canGoBack', () { + when(mockPlatformHostApi.canGoBack(webViewInstanceId)).thenReturn(true); + expect(webView.canGoBack(), completion(isTrue)); + }); + + test('canGoForward', () { + when(mockPlatformHostApi.canGoForward(webViewInstanceId)) + .thenReturn(false); + expect(webView.canGoForward(), completion(isFalse)); + }); + + test('goBack', () { + webView.goBack(); + verify(mockPlatformHostApi.goBack(webViewInstanceId)); + }); + + test('goForward', () { + webView.goForward(); + verify(mockPlatformHostApi.goForward(webViewInstanceId)); + }); + + test('reload', () { + webView.reload(); + verify(mockPlatformHostApi.reload(webViewInstanceId)); + }); + + test('getTitle', () { + when(mockPlatformHostApi.getTitle(webViewInstanceId)) + .thenReturn('MyTitle'); + expect(webView.getTitle(), completion('MyTitle')); + }); + + test('setAllowsBackForwardNavigationGestures', () { + webView.setAllowsBackForwardNavigationGestures(false); + verify(mockPlatformHostApi.setAllowsBackForwardNavigationGestures( + webViewInstanceId, + false, + )); + }); + + test('customUserAgent', () { + webView.setCustomUserAgent('hello'); + verify(mockPlatformHostApi.setCustomUserAgent( + webViewInstanceId, + 'hello', + )); + }); + + test('evaluateJavaScript', () { + when(mockPlatformHostApi.evaluateJavaScript(webViewInstanceId, 'gogo')) + .thenAnswer((_) => Future.value('stopstop')); + expect(webView.evaluateJavaScript('gogo'), completion('stopstop')); + }); + + test('evaluateJavaScript returns NSError', () { + when(mockPlatformHostApi.evaluateJavaScript(webViewInstanceId, 'gogo')) + .thenThrow( + PlatformException( + code: '', + details: NSErrorData( + code: 0, + domain: 'domain', + localizedDescription: 'desc', + ), + ), + ); + expect( + webView.evaluateJavaScript('gogo'), + throwsA( + isA().having( + (PlatformException exception) => exception.details, + 'details', + isA(), + ), + ), + ); + }); + }); + + group('WKUIDelegate', () { + late MockTestWKUIDelegateHostApi mockPlatformHostApi; + + late WKUIDelegate uiDelegate; + + setUp(() async { + mockPlatformHostApi = MockTestWKUIDelegateHostApi(); + TestWKUIDelegateHostApi.setup(mockPlatformHostApi); + + uiDelegate = WKUIDelegate(instanceManager: instanceManager); + }); + + tearDown(() { + TestWKUIDelegateHostApi.setup(null); + }); + + test('create', () async { + verify(mockPlatformHostApi.create( + instanceManager.getIdentifier(uiDelegate), + )); + }); + + test('onCreateWebView', () async { + final Completer> argsCompleter = + Completer>(); + + WebKitFlutterApis.instance = WebKitFlutterApis( + instanceManager: instanceManager, + ); + + uiDelegate = WKUIDelegate( + instanceManager: instanceManager, + onCreateWebView: ( + WKWebView webView, + WKWebViewConfiguration configuration, + WKNavigationAction navigationAction, + ) { + argsCompleter.complete([ + webView, + configuration, + navigationAction, + ]); + }, + ); + + final WKWebView webView = WKWebView.detached( + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance(webView, 2); + + final WKWebViewConfiguration configuration = + WKWebViewConfiguration.detached( + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance(configuration, 3); + + WebKitFlutterApis.instance.uiDelegate.onCreateWebView( + instanceManager.getIdentifier(uiDelegate)!, + 2, + 3, + WKNavigationActionData( + request: NSUrlRequestData( + url: 'url', + allHttpHeaderFields: {}, + ), + targetFrame: WKFrameInfoData(isMainFrame: false), + ), + ); + + expect( + argsCompleter.future, + completion([ + webView, + configuration, + isA(), + ]), + ); + }); + }); + }); +} 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 new file mode 100644 index 000000000000..18f30d434952 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart @@ -0,0 +1,313 @@ +// Mocks generated by Mockito 5.2.0 from annotations +// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart. +// Do not manually edit this file. + +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 '../common/test_web_kit.pigeon.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 + +/// A class which mocks [TestWKHttpCookieStoreHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKHttpCookieStoreHostApi extends _i1.Mock + implements _i2.TestWKHttpCookieStoreHostApi { + MockTestWKHttpCookieStoreHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void createFromWebsiteDataStore( + int? identifier, int? websiteDataStoreIdentifier) => + super.noSuchMethod( + Invocation.method(#createFromWebsiteDataStore, + [identifier, websiteDataStoreIdentifier]), + returnValueForMissingStub: null); + @override + _i3.Future setCookie(int? identifier, _i4.NSHttpCookieData? cookie) => + (super.noSuchMethod(Invocation.method(#setCookie, [identifier, cookie]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); +} + +/// A class which mocks [TestWKNavigationDelegateHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKNavigationDelegateHostApi extends _i1.Mock + implements _i2.TestWKNavigationDelegateHostApi { + MockTestWKNavigationDelegateHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? identifier) => + super.noSuchMethod(Invocation.method(#create, [identifier]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKPreferencesHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKPreferencesHostApi extends _i1.Mock + implements _i2.TestWKPreferencesHostApi { + MockTestWKPreferencesHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void createFromWebViewConfiguration( + int? identifier, int? configurationIdentifier) => + super.noSuchMethod( + Invocation.method(#createFromWebViewConfiguration, + [identifier, configurationIdentifier]), + returnValueForMissingStub: null); + @override + void setJavaScriptEnabled(int? identifier, bool? enabled) => + super.noSuchMethod( + Invocation.method(#setJavaScriptEnabled, [identifier, enabled]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKScriptMessageHandlerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKScriptMessageHandlerHostApi extends _i1.Mock + implements _i2.TestWKScriptMessageHandlerHostApi { + MockTestWKScriptMessageHandlerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? identifier) => + super.noSuchMethod(Invocation.method(#create, [identifier]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKUIDelegateHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKUIDelegateHostApi extends _i1.Mock + implements _i2.TestWKUIDelegateHostApi { + MockTestWKUIDelegateHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? identifier) => + super.noSuchMethod(Invocation.method(#create, [identifier]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKUserContentControllerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKUserContentControllerHostApi extends _i1.Mock + implements _i2.TestWKUserContentControllerHostApi { + MockTestWKUserContentControllerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void createFromWebViewConfiguration( + int? identifier, int? configurationIdentifier) => + super.noSuchMethod( + Invocation.method(#createFromWebViewConfiguration, + [identifier, configurationIdentifier]), + returnValueForMissingStub: null); + @override + void addScriptMessageHandler( + int? identifier, int? handlerIdentifier, String? name) => + super.noSuchMethod( + Invocation.method( + #addScriptMessageHandler, [identifier, handlerIdentifier, name]), + returnValueForMissingStub: null); + @override + void removeScriptMessageHandler(int? identifier, String? name) => + super.noSuchMethod( + Invocation.method(#removeScriptMessageHandler, [identifier, name]), + returnValueForMissingStub: null); + @override + void removeAllScriptMessageHandlers(int? identifier) => super.noSuchMethod( + Invocation.method(#removeAllScriptMessageHandlers, [identifier]), + returnValueForMissingStub: null); + @override + void addUserScript(int? identifier, _i4.WKUserScriptData? userScript) => super + .noSuchMethod(Invocation.method(#addUserScript, [identifier, userScript]), + returnValueForMissingStub: null); + @override + void removeAllUserScripts(int? identifier) => + super.noSuchMethod(Invocation.method(#removeAllUserScripts, [identifier]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKWebViewConfigurationHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKWebViewConfigurationHostApi extends _i1.Mock + implements _i2.TestWKWebViewConfigurationHostApi { + MockTestWKWebViewConfigurationHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? identifier) => + super.noSuchMethod(Invocation.method(#create, [identifier]), + returnValueForMissingStub: null); + @override + void createFromWebView(int? identifier, int? webViewIdentifier) => + super.noSuchMethod( + Invocation.method( + #createFromWebView, [identifier, webViewIdentifier]), + returnValueForMissingStub: null); + @override + void setAllowsInlineMediaPlayback(int? identifier, bool? allow) => + super.noSuchMethod( + Invocation.method(#setAllowsInlineMediaPlayback, [identifier, allow]), + returnValueForMissingStub: null); + @override + void setMediaTypesRequiringUserActionForPlayback( + int? identifier, List<_i4.WKAudiovisualMediaTypeEnumData?>? types) => + super.noSuchMethod( + Invocation.method(#setMediaTypesRequiringUserActionForPlayback, + [identifier, types]), + returnValueForMissingStub: null); +} + +/// A class which mocks [TestWKWebViewHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKWebViewHostApi extends _i1.Mock + implements _i2.TestWKWebViewHostApi { + MockTestWKWebViewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? identifier, int? configurationIdentifier) => + super.noSuchMethod( + Invocation.method(#create, [identifier, configurationIdentifier]), + returnValueForMissingStub: null); + @override + void setUIDelegate(int? identifier, int? uiDelegateIdentifier) => + super.noSuchMethod( + Invocation.method(#setUIDelegate, [identifier, uiDelegateIdentifier]), + returnValueForMissingStub: null); + @override + void setNavigationDelegate( + int? identifier, int? navigationDelegateIdentifier) => + super.noSuchMethod( + Invocation.method(#setNavigationDelegate, + [identifier, navigationDelegateIdentifier]), + returnValueForMissingStub: null); + @override + String? getUrl(int? identifier) => + (super.noSuchMethod(Invocation.method(#getUrl, [identifier])) as String?); + @override + double getEstimatedProgress(int? identifier) => (super.noSuchMethod( + Invocation.method(#getEstimatedProgress, [identifier]), + returnValue: 0.0) as double); + @override + void loadRequest(int? identifier, _i4.NSUrlRequestData? request) => + super.noSuchMethod(Invocation.method(#loadRequest, [identifier, request]), + returnValueForMissingStub: null); + @override + void loadHtmlString(int? identifier, String? string, String? baseUrl) => + super.noSuchMethod( + Invocation.method(#loadHtmlString, [identifier, string, baseUrl]), + returnValueForMissingStub: null); + @override + void loadFileUrl(int? identifier, String? url, String? readAccessUrl) => + super.noSuchMethod( + Invocation.method(#loadFileUrl, [identifier, url, readAccessUrl]), + returnValueForMissingStub: null); + @override + void loadFlutterAsset(int? identifier, String? key) => super.noSuchMethod( + Invocation.method(#loadFlutterAsset, [identifier, key]), + returnValueForMissingStub: null); + @override + bool canGoBack(int? identifier) => + (super.noSuchMethod(Invocation.method(#canGoBack, [identifier]), + returnValue: false) as bool); + @override + bool canGoForward(int? identifier) => + (super.noSuchMethod(Invocation.method(#canGoForward, [identifier]), + returnValue: false) as bool); + @override + void goBack(int? identifier) => + super.noSuchMethod(Invocation.method(#goBack, [identifier]), + returnValueForMissingStub: null); + @override + void goForward(int? identifier) => + super.noSuchMethod(Invocation.method(#goForward, [identifier]), + returnValueForMissingStub: null); + @override + void reload(int? identifier) => + super.noSuchMethod(Invocation.method(#reload, [identifier]), + returnValueForMissingStub: null); + @override + String? getTitle(int? identifier) => + (super.noSuchMethod(Invocation.method(#getTitle, [identifier])) + as String?); + @override + void setAllowsBackForwardNavigationGestures(int? identifier, bool? allow) => + super.noSuchMethod( + Invocation.method( + #setAllowsBackForwardNavigationGestures, [identifier, allow]), + returnValueForMissingStub: null); + @override + void setCustomUserAgent(int? identifier, String? userAgent) => + super.noSuchMethod( + Invocation.method(#setCustomUserAgent, [identifier, userAgent]), + returnValueForMissingStub: null); + @override + _i3.Future evaluateJavaScript( + int? identifier, String? javaScriptString) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavaScript, [identifier, javaScriptString]), + returnValue: Future.value()) as _i3.Future); +} + +/// A class which mocks [TestWKWebsiteDataStoreHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWKWebsiteDataStoreHostApi extends _i1.Mock + implements _i2.TestWKWebsiteDataStoreHostApi { + MockTestWKWebsiteDataStoreHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void createFromWebViewConfiguration( + int? identifier, int? configurationIdentifier) => + super.noSuchMethod( + Invocation.method(#createFromWebViewConfiguration, + [identifier, configurationIdentifier]), + returnValueForMissingStub: null); + @override + void createDefaultDataStore(int? identifier) => super.noSuchMethod( + Invocation.method(#createDefaultDataStore, [identifier]), + returnValueForMissingStub: null); + @override + _i3.Future removeDataOfTypes( + int? identifier, + List<_i4.WKWebsiteDataTypeEnumData?>? dataTypes, + double? modificationTimeInSecondsSinceEpoch) => + (super.noSuchMethod( + Invocation.method(#removeDataOfTypes, + [identifier, dataTypes, modificationTimeInSecondsSinceEpoch]), + returnValue: Future.value(false)) as _i3.Future); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.dart new file mode 100644 index 000000000000..73d8c8f33a11 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.dart @@ -0,0 +1,83 @@ +// 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: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/foundation/foundation.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; +import 'package:webview_flutter_wkwebview/src/wkwebview_cookie_manager.dart'; + +import 'web_kit_cookie_manager_test.mocks.dart'; + +@GenerateMocks([ + WKHttpCookieStore, + WKWebsiteDataStore, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('WebKitWebViewWidget', () { + late MockWKWebsiteDataStore mockWebsiteDataStore; + late MockWKHttpCookieStore mockWKHttpCookieStore; + + late WKWebViewCookieManager cookieManager; + + setUp(() { + mockWebsiteDataStore = MockWKWebsiteDataStore(); + mockWKHttpCookieStore = MockWKHttpCookieStore(); + when(mockWebsiteDataStore.httpCookieStore) + .thenReturn(mockWKHttpCookieStore); + + cookieManager = + WKWebViewCookieManager(websiteDataStore: mockWebsiteDataStore); + }); + + test('clearCookies', () async { + when(mockWebsiteDataStore.removeDataOfTypes( + {WKWebsiteDataType.cookies}, any)) + .thenAnswer((_) => Future.value(true)); + expect(cookieManager.clearCookies(), completion(true)); + + when(mockWebsiteDataStore.removeDataOfTypes( + {WKWebsiteDataType.cookies}, any)) + .thenAnswer((_) => Future.value(false)); + expect(cookieManager.clearCookies(), completion(false)); + }); + + test('setCookie', () async { + await cookieManager.setCookie( + const WebViewCookie(name: 'a', value: 'b', domain: 'c', path: 'd'), + ); + + final NSHttpCookie cookie = + verify(mockWKHttpCookieStore.setCookie(captureAny)).captured.single + as NSHttpCookie; + expect( + cookie.properties, + { + NSHttpCookiePropertyKey.name: 'a', + NSHttpCookiePropertyKey.value: 'b', + NSHttpCookiePropertyKey.domain: 'c', + NSHttpCookiePropertyKey.path: 'd', + }, + ); + }); + + test('setCookie throws argument error with invalid path', () async { + expect( + () => cookieManager.setCookie( + WebViewCookie( + name: 'a', + value: 'b', + domain: 'c', + path: String.fromCharCode(0x1F), + ), + ), + throwsArgumentError, + ); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.mocks.dart new file mode 100644 index 000000000000..e44e7b13a205 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.mocks.dart @@ -0,0 +1,100 @@ +// Mocks generated by Mockito 5.2.0 from annotations +// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' + as _i4; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.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 + +class _FakeWKHttpCookieStore_0 extends _i1.Fake + implements _i2.WKHttpCookieStore {} + +class _FakeWKWebsiteDataStore_1 extends _i1.Fake + implements _i2.WKWebsiteDataStore {} + +/// A class which mocks [WKHttpCookieStore]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKHttpCookieStore extends _i1.Mock implements _i2.WKHttpCookieStore { + MockWKHttpCookieStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future setCookie(_i4.NSHttpCookie? cookie) => + (super.noSuchMethod(Invocation.method(#setCookie, [cookie]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); + @override + _i2.WKHttpCookieStore copy() => + (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKHttpCookieStore_0()) as _i2.WKHttpCookieStore); + @override + _i3.Future addObserver(_i4.NSObject? observer, + {String? keyPath, Set<_i4.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); + @override + _i3.Future removeObserver(_i4.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); +} + +/// A class which mocks [WKWebsiteDataStore]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebsiteDataStore extends _i1.Mock + implements _i2.WKWebsiteDataStore { + MockWKWebsiteDataStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WKHttpCookieStore get httpCookieStore => + (super.noSuchMethod(Invocation.getter(#httpCookieStore), + returnValue: _FakeWKHttpCookieStore_0()) as _i2.WKHttpCookieStore); + @override + _i3.Future removeDataOfTypes( + Set<_i2.WKWebsiteDataType>? dataTypes, DateTime? since) => + (super.noSuchMethod( + Invocation.method(#removeDataOfTypes, [dataTypes, since]), + returnValue: Future.value(false)) as _i3.Future); + @override + _i2.WKWebsiteDataStore copy() => + (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKWebsiteDataStore_1()) as _i2.WKWebsiteDataStore); + @override + _i3.Future addObserver(_i4.NSObject? observer, + {String? keyPath, Set<_i4.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); + @override + _i3.Future removeObserver(_i4.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart index ab223ba5d9d0..5a3baa60a09c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart @@ -2,23 +2,32 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; +import 'dart:math'; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) +// ignore: unnecessary_import +import 'dart:typed_data'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:webview_pro_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_pro_wkwebview/src/foundation/foundation.dart'; +import 'package:webview_pro_wkwebview/src/ui_kit/ui_kit.dart'; import 'package:webview_pro_wkwebview/src/web_kit/web_kit.dart'; import 'package:webview_pro_wkwebview/src/web_kit_webview_widget.dart'; import 'web_kit_webview_widget_test.mocks.dart'; @GenerateMocks([ + UIScrollView, + WKNavigationDelegate, + WKPreferences, WKScriptMessageHandler, WKWebView, WKWebViewConfiguration, + WKWebsiteDataStore, WKUIDelegate, WKUserContentController, JavascriptChannelRegistry, @@ -28,12 +37,16 @@ import 'web_kit_webview_widget_test.mocks.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('$WebKitWebViewWidget', () { + group('WebKitWebViewWidget', () { late MockWKWebView mockWebView; late MockWebViewWidgetProxy mockWebViewWidgetProxy; late MockWKUserContentController mockUserContentController; + late MockWKPreferences mockPreferences; late MockWKWebViewConfiguration mockWebViewConfiguration; late MockWKUIDelegate mockUIDelegate; + late MockUIScrollView mockScrollView; + late MockWKWebsiteDataStore mockWebsiteDataStore; + late MockWKNavigationDelegate mockNavigationDelegate; late MockWebViewPlatformCallbacksHandler mockCallbacksHandler; late MockJavascriptChannelRegistry mockJavascriptChannelRegistry; @@ -44,15 +57,46 @@ void main() { mockWebView = MockWKWebView(); mockWebViewConfiguration = MockWKWebViewConfiguration(); mockUserContentController = MockWKUserContentController(); + mockPreferences = MockWKPreferences(); mockUIDelegate = MockWKUIDelegate(); + mockScrollView = MockUIScrollView(); + mockWebsiteDataStore = MockWKWebsiteDataStore(); + mockNavigationDelegate = MockWKNavigationDelegate(); mockWebViewWidgetProxy = MockWebViewWidgetProxy(); - when(mockWebViewWidgetProxy.createWebView(any)).thenReturn(mockWebView); - when(mockWebViewWidgetProxy.createUIDelgate()).thenReturn(mockUIDelegate); + when( + mockWebViewWidgetProxy.createWebView( + any, + observeValue: anyNamed('observeValue'), + ), + ).thenReturn(mockWebView); + when( + mockWebViewWidgetProxy.createUIDelgate( + onCreateWebView: captureAnyNamed('onCreateWebView'), + ), + ).thenReturn(mockUIDelegate); + when(mockWebViewWidgetProxy.createNavigationDelegate( + didFinishNavigation: anyNamed('didFinishNavigation'), + didStartProvisionalNavigation: + anyNamed('didStartProvisionalNavigation'), + decidePolicyForNavigationAction: + anyNamed('decidePolicyForNavigationAction'), + didFailNavigation: anyNamed('didFailNavigation'), + didFailProvisionalNavigation: anyNamed('didFailProvisionalNavigation'), + webViewWebContentProcessDidTerminate: + anyNamed('webViewWebContentProcessDidTerminate'), + )).thenReturn(mockNavigationDelegate); when(mockWebView.configuration).thenReturn(mockWebViewConfiguration); when(mockWebViewConfiguration.userContentController).thenReturn( mockUserContentController, ); + when(mockWebViewConfiguration.preferences).thenReturn(mockPreferences); + + when(mockWebView.scrollView).thenReturn(mockScrollView); + + when(mockWebViewConfiguration.websiteDataStore).thenReturn( + mockWebsiteDataStore, + ); mockCallbacksHandler = MockWebViewPlatformCallbacksHandler(); mockJavascriptChannelRegistry = MockJavascriptChannelRegistry(); @@ -93,12 +137,17 @@ void main() { (WidgetTester tester) async { await buildWidget(tester); - final dynamic onCreateWebView = - verify(mockUIDelegate.onCreateWebView = captureAny).captured.single - as void Function(WKWebViewConfiguration, WKNavigationAction); + final dynamic onCreateWebView = verify( + mockWebViewWidgetProxy.createUIDelgate( + onCreateWebView: captureAnyNamed('onCreateWebView'))) + .captured + .single + as void Function( + WKWebView, WKWebViewConfiguration, WKNavigationAction); const NSUrlRequest request = NSUrlRequest(url: 'https://google.com'); onCreateWebView( + mockWebView, mockWebViewConfiguration, const WKNavigationAction( request: request, @@ -109,13 +158,60 @@ void main() { verify(mockWebView.loadRequest(request)); }); - group('$CreationParams', () { + group('CreationParams', () { + testWidgets('initialUrl', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + initialUrl: 'https://www.google.com', + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: false, + ), + ), + ); + final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny)) + .captured + .single as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + }); + + testWidgets('backgroundColor', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + backgroundColor: Colors.red, + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebView.setOpaque(false)); + verify(mockWebView.setBackgroundColor(Colors.transparent)); + verify(mockScrollView.setBackgroundColor(Colors.red)); + }); + + testWidgets('userAgent', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + userAgent: 'MyUserAgent', + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebView.setCustomUserAgent('MyUserAgent')); + }); + testWidgets('autoMediaPlaybackPolicy true', (WidgetTester tester) async { await buildWidget( tester, creationParams: CreationParams( - autoMediaPlaybackPolicy: - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, webSettings: WebSettings( userAgent: const WebSetting.absent(), hasNavigationDelegate: false, @@ -123,11 +219,11 @@ void main() { ), ); - verify( - mockWebViewConfiguration.mediaTypesRequiringUserActionForPlayback = - { + verify(mockWebViewConfiguration + .setMediaTypesRequiringUserActionForPlayback(< + WKAudiovisualMediaType>{ WKAudiovisualMediaType.all, - }); + })); }); testWidgets('autoMediaPlaybackPolicy false', (WidgetTester tester) async { @@ -142,15 +238,19 @@ void main() { ), ); - verify( - mockWebViewConfiguration.mediaTypesRequiringUserActionForPlayback = - { + verify(mockWebViewConfiguration + .setMediaTypesRequiringUserActionForPlayback(< + WKAudiovisualMediaType>{ WKAudiovisualMediaType.none, - }); + })); }); testWidgets('javascriptChannelNames', (WidgetTester tester) async { - when(mockWebViewWidgetProxy.createScriptMessageHandler()).thenReturn( + when( + mockWebViewWidgetProxy.createScriptMessageHandler( + didReceiveScriptMessage: anyNamed('didReceiveScriptMessage'), + ), + ).thenReturn( MockWKScriptMessageHandler(), ); @@ -183,7 +283,146 @@ void main() { expect(javaScriptChannels[3], 'b'); }); - group('$WebSettings', () { + group('WebSettings', () { + testWidgets('javascriptMode', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + javascriptMode: JavascriptMode.unrestricted, + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockPreferences.setJavaScriptEnabled(true)); + }); + + testWidgets('userAgent', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.of('myUserAgent'), + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebView.setCustomUserAgent('myUserAgent')); + }); + + testWidgets( + 'enabling zoom re-adds JavaScript channels', + (WidgetTester tester) async { + when( + mockWebViewWidgetProxy.createScriptMessageHandler( + didReceiveScriptMessage: anyNamed('didReceiveScriptMessage'), + ), + ).thenReturn( + MockWKScriptMessageHandler(), + ); + + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + zoomEnabled: false, + hasNavigationDelegate: false, + ), + javascriptChannelNames: {'myChannel'}, + ), + ); + + clearInteractions(mockUserContentController); + + await testController.updateSettings(WebSettings( + userAgent: const WebSetting.absent(), + zoomEnabled: true, + )); + + final List javaScriptChannels = verifyInOrder([ + mockUserContentController.removeAllUserScripts(), + mockUserContentController.removeScriptMessageHandler('myChannel'), + mockUserContentController.addScriptMessageHandler( + captureAny, + captureAny, + ), + ]).captured[2]; + + expect( + javaScriptChannels[0], + isA(), + ); + expect(javaScriptChannels[1], 'myChannel'); + }, + ); + + testWidgets( + 'enabling zoom removes script', + (WidgetTester tester) async { + when(mockWebViewWidgetProxy.createScriptMessageHandler()) + .thenReturn( + MockWKScriptMessageHandler(), + ); + + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + zoomEnabled: false, + hasNavigationDelegate: false, + ), + ), + ); + + clearInteractions(mockUserContentController); + + await testController.updateSettings(WebSettings( + userAgent: const WebSetting.absent(), + zoomEnabled: true, + )); + + verify(mockUserContentController.removeAllUserScripts()); + verifyNever(mockUserContentController.addScriptMessageHandler( + any, + any, + )); + }, + ); + + testWidgets('zoomEnabled is false', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + zoomEnabled: false, + hasNavigationDelegate: false, + ), + ), + ); + + final WKUserScript zoomScript = + verify(mockUserContentController.addUserScript(captureAny)) + .captured + .first as WKUserScript; + expect(zoomScript.isMainFrameOnly, isTrue); + expect(zoomScript.injectionTime, + WKUserScriptInjectionTime.atDocumentEnd); + expect( + zoomScript.source, + "var meta = document.createElement('meta');\n" + "meta.name = 'viewport';\n" + "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, " + "user-scalable=no';\n" + "var head = document.getElementsByTagName('head')[0];head.appendChild(meta);", + ); + }); + testWidgets('allowsInlineMediaPlayback', (WidgetTester tester) async { await buildWidget( tester, @@ -195,14 +434,435 @@ void main() { ), ); - verify(mockWebViewConfiguration.allowsInlineMediaPlayback = true); + verify(mockWebViewConfiguration.setAllowsInlineMediaPlayback(true)); }); }); }); - group('$WebKitWebViewPlatformController', () { + group('WebKitWebViewPlatformController', () { + testWidgets('loadFile', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadFile('/path/to/file.html'); + verify(mockWebView.loadFileUrl( + '/path/to/file.html', + readAccessUrl: '/path/to', + )); + }); + + testWidgets('loadFlutterAsset', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadFlutterAsset('test_assets/index.html'); + verify(mockWebView.loadFlutterAsset('test_assets/index.html')); + }); + + testWidgets('loadHtmlString', (WidgetTester tester) async { + await buildWidget(tester); + + const String htmlString = 'Test data.'; + await testController.loadHtmlString(htmlString, baseUrl: 'baseUrl'); + + verify(mockWebView.loadHtmlString( + 'Test data.', + baseUrl: 'baseUrl', + )); + }); + + testWidgets('loadUrl', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadUrl( + 'https://www.google.com', + {'a': 'header'}, + ); + + final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny)) + .captured + .single as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.allHttpHeaderFields, {'a': 'header'}); + }); + + group('loadRequest', () { + testWidgets('Throws ArgumentError for empty scheme', + (WidgetTester tester) async { + await buildWidget(tester); + + expect( + () async => await testController.loadRequest( + WebViewRequest( + uri: Uri.parse('www.google.com'), + method: WebViewRequestMethod.get, + ), + ), + throwsA(const TypeMatcher())); + }); + + testWidgets('GET without headers', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadRequest(WebViewRequest( + uri: Uri.parse('https://www.google.com'), + method: WebViewRequestMethod.get, + )); + + final NSUrlRequest request = + verify(mockWebView.loadRequest(captureAny)).captured.single + as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.allHttpHeaderFields, {}); + expect(request.httpMethod, 'get'); + }); + + testWidgets('GET with headers', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadRequest(WebViewRequest( + uri: Uri.parse('https://www.google.com'), + method: WebViewRequestMethod.get, + headers: {'a': 'header'}, + )); + + final NSUrlRequest request = + verify(mockWebView.loadRequest(captureAny)).captured.single + as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.allHttpHeaderFields, {'a': 'header'}); + expect(request.httpMethod, 'get'); + }); + + testWidgets('POST without body', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadRequest(WebViewRequest( + uri: Uri.parse('https://www.google.com'), + method: WebViewRequestMethod.post, + )); + + final NSUrlRequest request = + verify(mockWebView.loadRequest(captureAny)).captured.single + as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.httpMethod, 'post'); + }); + + testWidgets('POST with body', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadRequest(WebViewRequest( + uri: Uri.parse('https://www.google.com'), + method: WebViewRequestMethod.post, + body: Uint8List.fromList('Test Body'.codeUnits))); + + final NSUrlRequest request = + verify(mockWebView.loadRequest(captureAny)).captured.single + as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.httpMethod, 'post'); + expect( + request.httpBody, + Uint8List.fromList('Test Body'.codeUnits), + ); + }); + }); + + testWidgets('canGoBack', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.canGoBack()).thenAnswer( + (_) => Future.value(false), + ); + expect(testController.canGoBack(), completion(false)); + }); + + testWidgets('canGoForward', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.canGoForward()).thenAnswer( + (_) => Future.value(true), + ); + expect(testController.canGoForward(), completion(true)); + }); + + testWidgets('goBack', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.goBack(); + verify(mockWebView.goBack()); + }); + + testWidgets('goForward', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.goForward(); + verify(mockWebView.goForward()); + }); + + testWidgets('reload', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.reload(); + verify(mockWebView.reload()); + }); + + testWidgets('evaluateJavascript', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value('returnString'), + ); + expect( + testController.evaluateJavascript('runJavaScript'), + completion('returnString'), + ); + }); + + testWidgets('evaluateJavascript with null return value', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value(), + ); + // The legacy implementation of webview_flutter_wkwebview would convert + // objects to strings before returning them to Dart. This verifies null + // is represented the way it is in Objective-C. + expect( + testController.evaluateJavascript('runJavaScript'), + completion('(null)'), + ); + }); + + testWidgets('evaluateJavascript with bool return value', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value(true), + ); + // The legacy implementation of webview_flutter_wkwebview would convert + // objects to strings before returning them to Dart. This verifies bool + // is represented the way it is in Objective-C. + // `NSNumber.description` converts bool values to a 1 or 0. + expect( + testController.evaluateJavascript('runJavaScript'), + completion('1'), + ); + }); + + testWidgets('evaluateJavascript with double return value', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value(1.0), + ); + // The legacy implementation of webview_flutter_wkwebview would convert + // objects to strings before returning them to Dart. This verifies + // double is represented the way it is in Objective-C. If a double + // doesn't contain any decimal values, it gets truncated to an int. + // This should be happenning because NSNumber convertes float values + // with no decimals to an int when using `NSNumber.description`. + expect( + testController.evaluateJavascript('runJavaScript'), + completion('1'), + ); + }); + + testWidgets('evaluateJavascript with list return value', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value([1, 'string', null]), + ); + // The legacy implementation of webview_flutter_wkwebview would convert + // objects to strings before returning them to Dart. This verifies list + // is represented the way it is in Objective-C. + expect( + testController.evaluateJavascript('runJavaScript'), + completion('(1,string,"")'), + ); + }); + + testWidgets('evaluateJavascript with map return value', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value({ + 1: 'string', + null: null, + }), + ); + // The legacy implementation of webview_flutter_wkwebview would convert + // objects to strings before returning them to Dart. This verifies map + // is represented the way it is in Objective-C. + expect( + testController.evaluateJavascript('runJavaScript'), + completion('{1 = string;"" = ""}'), + ); + }); + + testWidgets('evaluateJavascript throws exception', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')) + .thenThrow(Error()); + expect( + testController.evaluateJavascript('runJavaScript'), + throwsA(isA()), + ); + }); + + testWidgets('runJavascriptReturningResult', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value('returnString'), + ); + expect( + testController.runJavascriptReturningResult('runJavaScript'), + completion('returnString'), + ); + }); + + testWidgets( + 'runJavascriptReturningResult throws error on null return value', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value(), + ); + expect( + () => testController.runJavascriptReturningResult('runJavaScript'), + throwsArgumentError, + ); + }); + + testWidgets('runJavascriptReturningResult with bool return value', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value(false), + ); + // The legacy implementation of webview_flutter_wkwebview would convert + // objects to strings before returning them to Dart. This verifies bool + // is represented the way it is in Objective-C. + // `NSNumber.description` converts bool values to a 1 or 0. + expect( + testController.runJavascriptReturningResult('runJavaScript'), + completion('0'), + ); + }); + + testWidgets('runJavascript', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value('returnString'), + ); + expect( + testController.runJavascript('runJavaScript'), + completes, + ); + }); + + testWidgets( + 'runJavascript ignores exception with unsupported javascript type', + (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavaScript('runJavaScript')) + .thenThrow(PlatformException( + code: '', + details: const NSError( + code: WKErrorCode.javaScriptResultTypeIsUnsupported, + domain: '', + localizedDescription: '', + ), + )); + expect( + testController.runJavascript('runJavaScript'), + completes, + ); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.getTitle()) + .thenAnswer((_) => Future.value('Web Title')); + expect(testController.getTitle(), completion('Web Title')); + }); + + testWidgets('currentUrl', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.getUrl()) + .thenAnswer((_) => Future.value('myUrl.com')); + expect(testController.currentUrl(), completion('myUrl.com')); + }); + + testWidgets('scrollTo', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.scrollTo(2, 4); + verify(mockScrollView.setContentOffset(const Point(2.0, 4.0))); + }); + + testWidgets('scrollBy', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.scrollBy(2, 4); + verify(mockScrollView.scrollBy(const Point(2.0, 4.0))); + }); + + testWidgets('getScrollX', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockScrollView.getContentOffset()).thenAnswer( + (_) => Future>.value(const Point(8.0, 16.0))); + expect(testController.getScrollX(), completion(8.0)); + }); + + testWidgets('getScrollY', (WidgetTester tester) async { + await buildWidget(tester); + + await buildWidget(tester); + + when(mockScrollView.getContentOffset()).thenAnswer( + (_) => Future>.value(const Point(8.0, 16.0))); + expect(testController.getScrollY(), completion(16.0)); + }); + + testWidgets('clearCache', (WidgetTester tester) async { + await buildWidget(tester); + when( + mockWebsiteDataStore.removeDataOfTypes( + { + WKWebsiteDataType.memoryCache, + WKWebsiteDataType.diskCache, + WKWebsiteDataType.offlineWebApplicationCache, + WKWebsiteDataType.localStorage, + }, + DateTime.fromMillisecondsSinceEpoch(0), + ), + ).thenAnswer((_) => Future.value(false)); + + expect(testController.clearCache(), completes); + }); + testWidgets('addJavascriptChannels', (WidgetTester tester) async { - when(mockWebViewWidgetProxy.createScriptMessageHandler()).thenReturn( + when( + mockWebViewWidgetProxy.createScriptMessageHandler( + didReceiveScriptMessage: anyNamed('didReceiveScriptMessage'), + ), + ).thenReturn( MockWKScriptMessageHandler(), ); @@ -243,7 +903,11 @@ void main() { }); testWidgets('removeJavascriptChannels', (WidgetTester tester) async { - when(mockWebViewWidgetProxy.createScriptMessageHandler()).thenReturn( + when( + mockWebViewWidgetProxy.createScriptMessageHandler( + didReceiveScriptMessage: anyNamed('didReceiveScriptMessage'), + ), + ).thenReturn( MockWKScriptMessageHandler(), ); @@ -254,12 +918,15 @@ void main() { await testController.removeJavascriptChannels({'c'}); - verify(mockUserContentController.removeAllScriptMessageHandlers()); verify(mockUserContentController.removeAllUserScripts()); + verify(mockUserContentController.removeScriptMessageHandler('c')); + verify(mockUserContentController.removeScriptMessageHandler('d')); final List javaScriptChannels = verify( mockUserContentController.addScriptMessageHandler( - captureAny, captureAny), + captureAny, + captureAny, + ), ).captured; expect( javaScriptChannels[0], @@ -278,27 +945,304 @@ void main() { ); expect(userScripts[0].isMainFrameOnly, false); }); + + testWidgets('removeJavascriptChannels with zoom disabled', + (WidgetTester tester) async { + when( + mockWebViewWidgetProxy.createScriptMessageHandler( + didReceiveScriptMessage: anyNamed('didReceiveScriptMessage'), + ), + ).thenReturn( + MockWKScriptMessageHandler(), + ); + + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + zoomEnabled: false, + hasNavigationDelegate: false, + ), + ), + ); + + await testController.addJavascriptChannels({'c'}); + clearInteractions(mockUserContentController); + await testController.removeJavascriptChannels({'c'}); + + final WKUserScript zoomScript = + verify(mockUserContentController.addUserScript(captureAny)) + .captured + .first as WKUserScript; + expect(zoomScript.isMainFrameOnly, isTrue); + expect( + zoomScript.injectionTime, WKUserScriptInjectionTime.atDocumentEnd); + expect( + zoomScript.source, + "var meta = document.createElement('meta');\n" + "meta.name = 'viewport';\n" + "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, " + "user-scalable=no';\n" + "var head = document.getElementsByTagName('head')[0];head.appendChild(meta);", + ); + }); }); - group('$JavascriptChannelRegistry', () { + group('WebViewPlatformCallbacksHandler', () { + testWidgets('onPageStarted', (WidgetTester tester) async { + await buildWidget(tester); + + final dynamic didStartProvisionalNavigation = + verify(mockWebViewWidgetProxy.createNavigationDelegate( + didFinishNavigation: anyNamed('didFinishNavigation'), + didStartProvisionalNavigation: + captureAnyNamed('didStartProvisionalNavigation'), + decidePolicyForNavigationAction: + anyNamed('decidePolicyForNavigationAction'), + didFailNavigation: anyNamed('didFailNavigation'), + didFailProvisionalNavigation: + anyNamed('didFailProvisionalNavigation'), + webViewWebContentProcessDidTerminate: + anyNamed('webViewWebContentProcessDidTerminate'), + )).captured.single as void Function(WKWebView, String); + didStartProvisionalNavigation(mockWebView, 'https://google.com'); + + verify(mockCallbacksHandler.onPageStarted('https://google.com')); + }); + + testWidgets('onPageFinished', (WidgetTester tester) async { + await buildWidget(tester); + + final dynamic didFinishNavigation = + verify(mockWebViewWidgetProxy.createNavigationDelegate( + didFinishNavigation: captureAnyNamed('didFinishNavigation'), + didStartProvisionalNavigation: + anyNamed('didStartProvisionalNavigation'), + decidePolicyForNavigationAction: + anyNamed('decidePolicyForNavigationAction'), + didFailNavigation: anyNamed('didFailNavigation'), + didFailProvisionalNavigation: + anyNamed('didFailProvisionalNavigation'), + webViewWebContentProcessDidTerminate: + anyNamed('webViewWebContentProcessDidTerminate'), + )).captured.single as void Function(WKWebView, String); + didFinishNavigation(mockWebView, 'https://google.com'); + + verify(mockCallbacksHandler.onPageFinished('https://google.com')); + }); + + testWidgets('onWebResourceError from didFailNavigation', + (WidgetTester tester) async { + await buildWidget(tester); + + final dynamic didFailNavigation = + verify(mockWebViewWidgetProxy.createNavigationDelegate( + didFinishNavigation: anyNamed('didFinishNavigation'), + didStartProvisionalNavigation: + anyNamed('didStartProvisionalNavigation'), + decidePolicyForNavigationAction: + anyNamed('decidePolicyForNavigationAction'), + didFailNavigation: captureAnyNamed('didFailNavigation'), + didFailProvisionalNavigation: + anyNamed('didFailProvisionalNavigation'), + webViewWebContentProcessDidTerminate: + anyNamed('webViewWebContentProcessDidTerminate'), + )).captured.single as void Function(WKWebView, NSError); + + didFailNavigation( + mockWebView, + const NSError( + code: WKErrorCode.webViewInvalidated, + domain: 'domain', + localizedDescription: 'my desc', + ), + ); + + final WebResourceError error = + verify(mockCallbacksHandler.onWebResourceError(captureAny)) + .captured + .single as WebResourceError; + expect(error.description, 'my desc'); + expect(error.errorCode, WKErrorCode.webViewInvalidated); + expect(error.domain, 'domain'); + expect(error.errorType, WebResourceErrorType.webViewInvalidated); + }); + + testWidgets('onWebResourceError from didFailProvisionalNavigation', + (WidgetTester tester) async { + await buildWidget(tester); + + final dynamic didFailProvisionalNavigation = + verify(mockWebViewWidgetProxy.createNavigationDelegate( + didFinishNavigation: anyNamed('didFinishNavigation'), + didStartProvisionalNavigation: + anyNamed('didStartProvisionalNavigation'), + decidePolicyForNavigationAction: + anyNamed('decidePolicyForNavigationAction'), + didFailNavigation: anyNamed('didFailNavigation'), + didFailProvisionalNavigation: + captureAnyNamed('didFailProvisionalNavigation'), + webViewWebContentProcessDidTerminate: + anyNamed('webViewWebContentProcessDidTerminate'), + )).captured.single as void Function(WKWebView, NSError); + + didFailProvisionalNavigation( + mockWebView, + const NSError( + code: WKErrorCode.webContentProcessTerminated, + domain: 'domain', + localizedDescription: 'my desc', + ), + ); + + final WebResourceError error = + verify(mockCallbacksHandler.onWebResourceError(captureAny)) + .captured + .single as WebResourceError; + expect(error.description, 'my desc'); + expect(error.errorCode, WKErrorCode.webContentProcessTerminated); + expect(error.domain, 'domain'); + expect( + error.errorType, + WebResourceErrorType.webContentProcessTerminated, + ); + }); + + testWidgets( + 'onWebResourceError from webViewWebContentProcessDidTerminate', + (WidgetTester tester) async { + await buildWidget(tester); + + final dynamic webViewWebContentProcessDidTerminate = + verify(mockWebViewWidgetProxy.createNavigationDelegate( + didFinishNavigation: anyNamed('didFinishNavigation'), + didStartProvisionalNavigation: + anyNamed('didStartProvisionalNavigation'), + decidePolicyForNavigationAction: + anyNamed('decidePolicyForNavigationAction'), + didFailNavigation: anyNamed('didFailNavigation'), + didFailProvisionalNavigation: + anyNamed('didFailProvisionalNavigation'), + webViewWebContentProcessDidTerminate: + captureAnyNamed('webViewWebContentProcessDidTerminate'), + )).captured.single as void Function(WKWebView); + webViewWebContentProcessDidTerminate(mockWebView); + + final WebResourceError error = + verify(mockCallbacksHandler.onWebResourceError(captureAny)) + .captured + .single as WebResourceError; + expect(error.description, ''); + expect(error.errorCode, WKErrorCode.webContentProcessTerminated); + expect(error.domain, 'WKErrorDomain'); + expect( + error.errorType, + WebResourceErrorType.webContentProcessTerminated, + ); + }); + + testWidgets('onNavigationRequest from decidePolicyForNavigationAction', + (WidgetTester tester) async { + await buildWidget(tester, hasNavigationDelegate: true); + + final dynamic decidePolicyForNavigationAction = + verify(mockWebViewWidgetProxy.createNavigationDelegate( + didFinishNavigation: anyNamed('didFinishNavigation'), + didStartProvisionalNavigation: + anyNamed('didStartProvisionalNavigation'), + decidePolicyForNavigationAction: + captureAnyNamed('decidePolicyForNavigationAction'), + didFailNavigation: anyNamed('didFailNavigation'), + didFailProvisionalNavigation: + anyNamed('didFailProvisionalNavigation'), + webViewWebContentProcessDidTerminate: + anyNamed('webViewWebContentProcessDidTerminate'), + )).captured.single as Future Function( + WKWebView, WKNavigationAction); + + when(mockCallbacksHandler.onNavigationRequest( + isForMainFrame: argThat(isFalse, named: 'isForMainFrame'), + url: 'https://google.com', + )).thenReturn(true); + + expect( + decidePolicyForNavigationAction( + mockWebView, + const WKNavigationAction( + request: NSUrlRequest(url: 'https://google.com'), + targetFrame: WKFrameInfo(isMainFrame: false), + ), + ), + completion(WKNavigationActionPolicy.allow), + ); + + verify(mockCallbacksHandler.onNavigationRequest( + url: 'https://google.com', + isForMainFrame: false, + )); + }); + + testWidgets('onProgress', (WidgetTester tester) async { + await buildWidget(tester, hasProgressTracking: true); + + verify(mockWebView.addObserver( + mockWebView, + keyPath: 'estimatedProgress', + options: { + NSKeyValueObservingOptions.newValue, + }, + )); + + final dynamic observeValue = verify( + mockWebViewWidgetProxy.createWebView(any, + observeValue: captureAnyNamed('observeValue'))) + .captured + .single as void Function( + String keyPath, + NSObject object, + Map change, + ); + + observeValue( + 'estimatedProgress', + mockWebView, + {NSKeyValueChangeKey.newValue: 0.32}, + ); + + verify(mockCallbacksHandler.onProgress(32)); + }); + + testWidgets('progress observer is not removed without being set first', + (WidgetTester tester) async { + await buildWidget(tester); + + verifyNever(mockWebView.removeObserver( + mockWebView, + keyPath: 'estimatedProgress', + )); + }); + }); + + group('JavascriptChannelRegistry', () { testWidgets('onJavascriptChannelMessage', (WidgetTester tester) async { - when(mockWebViewWidgetProxy.createScriptMessageHandler()).thenReturn( + when( + mockWebViewWidgetProxy.createScriptMessageHandler( + didReceiveScriptMessage: anyNamed('didReceiveScriptMessage'), + ), + ).thenReturn( MockWKScriptMessageHandler(), ); await buildWidget(tester); await testController.addJavascriptChannels({'hello'}); - final MockWKScriptMessageHandler messageHandler = verify( - mockUserContentController.addScriptMessageHandler( - captureAny, 'hello')) + final dynamic didReceiveScriptMessage = verify( + mockWebViewWidgetProxy.createScriptMessageHandler( + didReceiveScriptMessage: + captureAnyNamed('didReceiveScriptMessage'))) .captured - .single as MockWKScriptMessageHandler; - - final dynamic didReceiveScriptMessage = - verify(messageHandler.didReceiveScriptMessage = captureAny) - .captured - .single as void Function( + .single as void Function( WKUserContentController userContentController, WKScriptMessage message, ); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.mocks.dart index 33c8add9cdc3..9cf7a0d58878 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.mocks.dart @@ -1,21 +1,26 @@ -// Mocks generated by Mockito 5.0.17 from annotations -// in webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart. +// Mocks generated by Mockito 5.2.0 from annotations +// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart. // Do not manually edit this file. -import 'dart:async' as _i3; +import 'dart:async' as _i5; +import 'dart:math' as _i2; +import 'dart:ui' as _i6; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_pro_platform_interface/src/types/javascript_channel.dart' - as _i6; -import 'package:webview_pro_platform_interface/src/types/types.dart' as _i7; + as _i9; +import 'package:webview_pro_platform_interface/src/types/types.dart' + as _i10; import 'package:webview_pro_platform_interface/webview_flutter_platform_interface.dart' - as _i5; + as _i8; import 'package:webview_pro_wkwebview/src/foundation/foundation.dart' - as _i4; -import 'package:webview_pro_wkwebview/src/web_kit/web_kit.dart' as _i2; + as _i7; +import 'package:webview_pro_wkwebview/src/ui_kit/ui_kit.dart' as _i3; +import 'package:webview_pro_wkwebview/src/web_kit/web_kit.dart' as _i4; import 'package:webview_pro_wkwebview/src/web_kit_webview_widget.dart' - as _i8; + as _i11; +// ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references @@ -25,168 +30,522 @@ import 'package:webview_pro_wkwebview/src/web_kit_webview_widget.dart' // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types -class _FakeWKWebViewConfiguration_0 extends _i1.Fake - implements _i2.WKWebViewConfiguration {} +class _FakePoint_0 extends _i1.Fake implements _i2.Point {} + +class _FakeUIScrollView_1 extends _i1.Fake implements _i3.UIScrollView {} + +class _FakeWKNavigationDelegate_2 extends _i1.Fake + implements _i4.WKNavigationDelegate {} + +class _FakeWKPreferences_3 extends _i1.Fake implements _i4.WKPreferences {} + +class _FakeWKScriptMessageHandler_4 extends _i1.Fake + implements _i4.WKScriptMessageHandler {} + +class _FakeWKWebViewConfiguration_5 extends _i1.Fake + implements _i4.WKWebViewConfiguration {} + +class _FakeWKWebView_6 extends _i1.Fake implements _i4.WKWebView {} + +class _FakeWKUserContentController_7 extends _i1.Fake + implements _i4.WKUserContentController {} + +class _FakeWKWebsiteDataStore_8 extends _i1.Fake + implements _i4.WKWebsiteDataStore {} + +class _FakeWKHttpCookieStore_9 extends _i1.Fake + implements _i4.WKHttpCookieStore {} -class _FakeWKUserContentController_1 extends _i1.Fake - implements _i2.WKUserContentController {} +class _FakeWKUIDelegate_10 extends _i1.Fake implements _i4.WKUIDelegate {} -class _FakeWKWebView_2 extends _i1.Fake implements _i2.WKWebView {} +/// A class which mocks [UIScrollView]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockUIScrollView extends _i1.Mock implements _i3.UIScrollView { + MockUIScrollView() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future<_i2.Point> getContentOffset() => (super.noSuchMethod( + Invocation.method(#getContentOffset, []), + returnValue: Future<_i2.Point>.value(_FakePoint_0())) + as _i5.Future<_i2.Point>); + @override + _i5.Future scrollBy(_i2.Point? offset) => + (super.noSuchMethod(Invocation.method(#scrollBy, [offset]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setContentOffset(_i2.Point? offset) => + (super.noSuchMethod(Invocation.method(#setContentOffset, [offset]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i3.UIScrollView copy() => (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeUIScrollView_1()) as _i3.UIScrollView); + @override + _i5.Future setBackgroundColor(_i6.Color? color) => + (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setOpaque(bool? opaque) => + (super.noSuchMethod(Invocation.method(#setOpaque, [opaque]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); +} + +/// A class which mocks [WKNavigationDelegate]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKNavigationDelegate extends _i1.Mock + implements _i4.WKNavigationDelegate { + MockWKNavigationDelegate() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKNavigationDelegate copy() => (super.noSuchMethod( + Invocation.method(#copy, []), + returnValue: _FakeWKNavigationDelegate_2()) as _i4.WKNavigationDelegate); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); +} -class _FakeWKScriptMessageHandler_3 extends _i1.Fake - implements _i2.WKScriptMessageHandler {} +/// A class which mocks [WKPreferences]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKPreferences extends _i1.Mock implements _i4.WKPreferences { + MockWKPreferences() { + _i1.throwOnMissingStub(this); + } -class _FakeWKUIDelegate_4 extends _i1.Fake implements _i2.WKUIDelegate {} + @override + _i5.Future setJavaScriptEnabled(bool? enabled) => + (super.noSuchMethod(Invocation.method(#setJavaScriptEnabled, [enabled]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i4.WKPreferences copy() => (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKPreferences_3()) as _i4.WKPreferences); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); +} /// A class which mocks [WKScriptMessageHandler]. /// /// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable class MockWKScriptMessageHandler extends _i1.Mock - implements _i2.WKScriptMessageHandler { + implements _i4.WKScriptMessageHandler { MockWKScriptMessageHandler() { _i1.throwOnMissingStub(this); } @override - set didReceiveScriptMessage( - void Function(_i2.WKUserContentController, _i2.WKScriptMessage)? - didReceiveScriptMessage) => - super.noSuchMethod( - Invocation.setter(#didReceiveScriptMessage, didReceiveScriptMessage), - returnValueForMissingStub: null); + void Function(_i4.WKUserContentController, _i4.WKScriptMessage) + get didReceiveScriptMessage => + (super.noSuchMethod(Invocation.getter(#didReceiveScriptMessage), + returnValue: (_i4.WKUserContentController userContentController, + _i4.WKScriptMessage message) {}) as void Function( + _i4.WKUserContentController, _i4.WKScriptMessage)); + @override + _i4.WKScriptMessageHandler copy() => + (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKScriptMessageHandler_4()) + as _i4.WKScriptMessageHandler); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); } /// A class which mocks [WKWebView]. /// /// See the documentation for Mockito's code generation for more information. -class MockWKWebView extends _i1.Mock implements _i2.WKWebView { +// ignore: must_be_immutable +class MockWKWebView extends _i1.Mock implements _i4.WKWebView { MockWKWebView() { _i1.throwOnMissingStub(this); } @override - _i2.WKWebViewConfiguration get configuration => + _i4.WKWebViewConfiguration get configuration => (super.noSuchMethod(Invocation.getter(#configuration), - returnValue: _FakeWKWebViewConfiguration_0()) - as _i2.WKWebViewConfiguration); + returnValue: _FakeWKWebViewConfiguration_5()) + as _i4.WKWebViewConfiguration); @override - set uiDelegate(_i2.WKUIDelegate? delegate) => - super.noSuchMethod(Invocation.setter(#uiDelegate, delegate), - returnValueForMissingStub: null); + _i3.UIScrollView get scrollView => + (super.noSuchMethod(Invocation.getter(#scrollView), + returnValue: _FakeUIScrollView_1()) as _i3.UIScrollView); + @override + _i5.Future setUIDelegate(_i4.WKUIDelegate? delegate) => + (super.noSuchMethod(Invocation.method(#setUIDelegate, [delegate]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setNavigationDelegate(_i4.WKNavigationDelegate? delegate) => + (super.noSuchMethod(Invocation.method(#setNavigationDelegate, [delegate]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future getUrl() => + (super.noSuchMethod(Invocation.method(#getUrl, []), + returnValue: Future.value()) as _i5.Future); + @override + _i5.Future getEstimatedProgress() => + (super.noSuchMethod(Invocation.method(#getEstimatedProgress, []), + returnValue: Future.value(0.0)) as _i5.Future); @override - _i3.Future loadRequest(_i4.NSUrlRequest? request) => + _i5.Future loadRequest(_i7.NSUrlRequest? request) => (super.noSuchMethod(Invocation.method(#loadRequest, [request]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future loadHtmlString(String? string, {String? baseUrl}) => + (super.noSuchMethod( + Invocation.method(#loadHtmlString, [string], {#baseUrl: baseUrl}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future loadFileUrl(String? url, {String? readAccessUrl}) => + (super.noSuchMethod( + Invocation.method( + #loadFileUrl, [url], {#readAccessUrl: readAccessUrl}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future loadFlutterAsset(String? key) => + (super.noSuchMethod(Invocation.method(#loadFlutterAsset, [key]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future canGoBack() => + (super.noSuchMethod(Invocation.method(#canGoBack, []), + returnValue: Future.value(false)) as _i5.Future); + @override + _i5.Future canGoForward() => + (super.noSuchMethod(Invocation.method(#canGoForward, []), + returnValue: Future.value(false)) as _i5.Future); + @override + _i5.Future goBack() => + (super.noSuchMethod(Invocation.method(#goBack, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future goForward() => + (super.noSuchMethod(Invocation.method(#goForward, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future reload() => + (super.noSuchMethod(Invocation.method(#reload, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future getTitle() => + (super.noSuchMethod(Invocation.method(#getTitle, []), + returnValue: Future.value()) as _i5.Future); + @override + _i5.Future setAllowsBackForwardNavigationGestures(bool? allow) => + (super.noSuchMethod( + Invocation.method(#setAllowsBackForwardNavigationGestures, [allow]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setCustomUserAgent(String? userAgent) => + (super.noSuchMethod(Invocation.method(#setCustomUserAgent, [userAgent]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future evaluateJavaScript(String? javaScriptString) => (super + .noSuchMethod(Invocation.method(#evaluateJavaScript, [javaScriptString]), + returnValue: Future.value()) as _i5.Future); + @override + _i4.WKWebView copy() => (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKWebView_6()) as _i4.WKWebView); + @override + _i5.Future setBackgroundColor(_i6.Color? color) => + (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setOpaque(bool? opaque) => + (super.noSuchMethod(Invocation.method(#setOpaque, [opaque]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); } /// A class which mocks [WKWebViewConfiguration]. /// /// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable class MockWKWebViewConfiguration extends _i1.Mock - implements _i2.WKWebViewConfiguration { + implements _i4.WKWebViewConfiguration { MockWKWebViewConfiguration() { _i1.throwOnMissingStub(this); } @override - _i2.WKUserContentController get userContentController => + _i4.WKUserContentController get userContentController => (super.noSuchMethod(Invocation.getter(#userContentController), - returnValue: _FakeWKUserContentController_1()) - as _i2.WKUserContentController); + returnValue: _FakeWKUserContentController_7()) + as _i4.WKUserContentController); @override - set userContentController( - _i2.WKUserContentController? _userContentController) => - super.noSuchMethod( - Invocation.setter(#userContentController, _userContentController), - returnValueForMissingStub: null); + _i4.WKPreferences get preferences => + (super.noSuchMethod(Invocation.getter(#preferences), + returnValue: _FakeWKPreferences_3()) as _i4.WKPreferences); @override - set allowsInlineMediaPlayback(bool? allow) => - super.noSuchMethod(Invocation.setter(#allowsInlineMediaPlayback, allow), - returnValueForMissingStub: null); + _i4.WKWebsiteDataStore get websiteDataStore => + (super.noSuchMethod(Invocation.getter(#websiteDataStore), + returnValue: _FakeWKWebsiteDataStore_8()) as _i4.WKWebsiteDataStore); @override - set mediaTypesRequiringUserActionForPlayback( - Set<_i2.WKAudiovisualMediaType>? types) => - super.noSuchMethod( - Invocation.setter(#mediaTypesRequiringUserActionForPlayback, types), - returnValueForMissingStub: null); + _i5.Future setAllowsInlineMediaPlayback(bool? allow) => (super + .noSuchMethod(Invocation.method(#setAllowsInlineMediaPlayback, [allow]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setMediaTypesRequiringUserActionForPlayback( + Set<_i4.WKAudiovisualMediaType>? types) => + (super.noSuchMethod( + Invocation.method( + #setMediaTypesRequiringUserActionForPlayback, [types]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i4.WKWebViewConfiguration copy() => + (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKWebViewConfiguration_5()) + as _i4.WKWebViewConfiguration); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); +} + +/// A class which mocks [WKWebsiteDataStore]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebsiteDataStore extends _i1.Mock + implements _i4.WKWebsiteDataStore { + MockWKWebsiteDataStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKHttpCookieStore get httpCookieStore => + (super.noSuchMethod(Invocation.getter(#httpCookieStore), + returnValue: _FakeWKHttpCookieStore_9()) as _i4.WKHttpCookieStore); + @override + _i5.Future removeDataOfTypes( + Set<_i4.WKWebsiteDataType>? dataTypes, DateTime? since) => + (super.noSuchMethod( + Invocation.method(#removeDataOfTypes, [dataTypes, since]), + returnValue: Future.value(false)) as _i5.Future); + @override + _i4.WKWebsiteDataStore copy() => + (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKWebsiteDataStore_8()) as _i4.WKWebsiteDataStore); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); } /// A class which mocks [WKUIDelegate]. /// /// See the documentation for Mockito's code generation for more information. -class MockWKUIDelegate extends _i1.Mock implements _i2.WKUIDelegate { +// ignore: must_be_immutable +class MockWKUIDelegate extends _i1.Mock implements _i4.WKUIDelegate { MockWKUIDelegate() { _i1.throwOnMissingStub(this); } @override - set onCreateWebView( - void Function(_i2.WKWebViewConfiguration, _i2.WKNavigationAction)? - onCreateeWebView) => - super.noSuchMethod(Invocation.setter(#onCreateWebView, onCreateeWebView), - returnValueForMissingStub: null); + _i4.WKUIDelegate copy() => (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKUIDelegate_10()) as _i4.WKUIDelegate); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); } /// A class which mocks [WKUserContentController]. /// /// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable class MockWKUserContentController extends _i1.Mock - implements _i2.WKUserContentController { + implements _i4.WKUserContentController { MockWKUserContentController() { _i1.throwOnMissingStub(this); } @override - _i3.Future addScriptMessageHandler( - _i2.WKScriptMessageHandler? handler, String? name) => + _i5.Future addScriptMessageHandler( + _i4.WKScriptMessageHandler? handler, String? name) => (super.noSuchMethod( Invocation.method(#addScriptMessageHandler, [handler, name]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i3.Future removeScriptMessageHandler(String? name) => (super + _i5.Future removeScriptMessageHandler(String? name) => (super .noSuchMethod(Invocation.method(#removeScriptMessageHandler, [name]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i3.Future removeAllScriptMessageHandlers() => (super.noSuchMethod( + _i5.Future removeAllScriptMessageHandlers() => (super.noSuchMethod( Invocation.method(#removeAllScriptMessageHandlers, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i3.Future addUserScript(_i2.WKUserScript? userScript) => + _i5.Future addUserScript(_i4.WKUserScript? userScript) => (super.noSuchMethod(Invocation.method(#addUserScript, [userScript]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i3.Future removeAllUserScripts() => + _i5.Future removeAllUserScripts() => (super.noSuchMethod(Invocation.method(#removeAllUserScripts, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i4.WKUserContentController copy() => + (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKUserContentController_7()) + as _i4.WKUserContentController); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); } /// A class which mocks [JavascriptChannelRegistry]. /// /// See the documentation for Mockito's code generation for more information. class MockJavascriptChannelRegistry extends _i1.Mock - implements _i5.JavascriptChannelRegistry { + implements _i8.JavascriptChannelRegistry { MockJavascriptChannelRegistry() { _i1.throwOnMissingStub(this); } @override - Map get channels => + Map get channels => (super.noSuchMethod(Invocation.getter(#channels), - returnValue: {}) - as Map); + returnValue: {}) + as Map); @override void onJavascriptChannelMessage(String? channel, String? message) => super.noSuchMethod( Invocation.method(#onJavascriptChannelMessage, [channel, message]), returnValueForMissingStub: null); @override - void updateJavascriptChannelsFromSet(Set<_i6.JavascriptChannel>? channels) => + void updateJavascriptChannelsFromSet(Set<_i9.JavascriptChannel>? channels) => super.noSuchMethod( Invocation.method(#updateJavascriptChannelsFromSet, [channels]), returnValueForMissingStub: null); @@ -196,17 +555,17 @@ class MockJavascriptChannelRegistry extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockWebViewPlatformCallbacksHandler extends _i1.Mock - implements _i5.WebViewPlatformCallbacksHandler { + implements _i8.WebViewPlatformCallbacksHandler { MockWebViewPlatformCallbacksHandler() { _i1.throwOnMissingStub(this); } @override - _i3.FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) => + _i5.FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) => (super.noSuchMethod( Invocation.method(#onNavigationRequest, [], {#url: url, #isForMainFrame: isForMainFrame}), - returnValue: Future.value(false)) as _i3.FutureOr); + returnValue: Future.value(false)) as _i5.FutureOr); @override void onPageStarted(String? url) => super.noSuchMethod(Invocation.method(#onPageStarted, [url]), @@ -220,7 +579,7 @@ class MockWebViewPlatformCallbacksHandler extends _i1.Mock super.noSuchMethod(Invocation.method(#onProgress, [progress]), returnValueForMissingStub: null); @override - void onWebResourceError(_i7.WebResourceError? error) => + void onWebResourceError(_i10.WebResourceError? error) => super.noSuchMethod(Invocation.method(#onWebResourceError, [error]), returnValueForMissingStub: null); } @@ -229,22 +588,61 @@ class MockWebViewPlatformCallbacksHandler extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockWebViewWidgetProxy extends _i1.Mock - implements _i8.WebViewWidgetProxy { + implements _i11.WebViewWidgetProxy { MockWebViewWidgetProxy() { _i1.throwOnMissingStub(this); } @override - _i2.WKWebView createWebView(_i2.WKWebViewConfiguration? configuration) => - (super.noSuchMethod(Invocation.method(#createWebView, [configuration]), - returnValue: _FakeWKWebView_2()) as _i2.WKWebView); + _i4.WKWebView createWebView(_i4.WKWebViewConfiguration? configuration, + {void Function( + String, _i7.NSObject, Map<_i7.NSKeyValueChangeKey, Object?>)? + observeValue}) => + (super.noSuchMethod( + Invocation.method( + #createWebView, [configuration], {#observeValue: observeValue}), + returnValue: _FakeWKWebView_6()) as _i4.WKWebView); @override - _i2.WKScriptMessageHandler createScriptMessageHandler() => - (super.noSuchMethod(Invocation.method(#createScriptMessageHandler, []), - returnValue: _FakeWKScriptMessageHandler_3()) - as _i2.WKScriptMessageHandler); + _i4.WKScriptMessageHandler createScriptMessageHandler( + {void Function(_i4.WKUserContentController, _i4.WKScriptMessage)? + didReceiveScriptMessage}) => + (super.noSuchMethod( + Invocation.method(#createScriptMessageHandler, [], + {#didReceiveScriptMessage: didReceiveScriptMessage}), + returnValue: _FakeWKScriptMessageHandler_4()) + as _i4.WKScriptMessageHandler); @override - _i2.WKUIDelegate createUIDelgate() => - (super.noSuchMethod(Invocation.method(#createUIDelgate, []), - returnValue: _FakeWKUIDelegate_4()) as _i2.WKUIDelegate); + _i4.WKUIDelegate createUIDelgate( + {void Function(_i4.WKWebView, _i4.WKWebViewConfiguration, + _i4.WKNavigationAction)? + onCreateWebView}) => + (super.noSuchMethod( + Invocation.method( + #createUIDelgate, [], {#onCreateWebView: onCreateWebView}), + returnValue: _FakeWKUIDelegate_10()) as _i4.WKUIDelegate); + @override + _i4.WKNavigationDelegate createNavigationDelegate( + {void Function(_i4.WKWebView, String?)? didFinishNavigation, + void Function(_i4.WKWebView, String?)? didStartProvisionalNavigation, + _i5.Future<_i4.WKNavigationActionPolicy> Function( + _i4.WKWebView, _i4.WKNavigationAction)? + decidePolicyForNavigationAction, + void Function(_i4.WKWebView, _i7.NSError)? didFailNavigation, + void Function(_i4.WKWebView, _i7.NSError)? + didFailProvisionalNavigation, + void Function(_i4.WKWebView)? + webViewWebContentProcessDidTerminate}) => + (super.noSuchMethod( + Invocation.method(#createNavigationDelegate, [], { + #didFinishNavigation: didFinishNavigation, + #didStartProvisionalNavigation: didStartProvisionalNavigation, + #decidePolicyForNavigationAction: + decidePolicyForNavigationAction, + #didFailNavigation: didFailNavigation, + #didFailProvisionalNavigation: didFailProvisionalNavigation, + #webViewWebContentProcessDidTerminate: + webViewWebContentProcessDidTerminate + }), + returnValue: _FakeWKNavigationDelegate_2()) + as _i4.WKNavigationDelegate); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/wkwebview_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/wkwebview_cookie_manager_test.dart deleted file mode 100644 index 4c32e457cbda..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/wkwebview_cookie_manager_test.dart +++ /dev/null @@ -1,65 +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/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_pro_platform_interface/webview_flutter_platform_interface.dart'; -import 'package:webview_pro_wkwebview/src/wkwebview_cookie_manager.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - const MethodChannel cookieChannel = - MethodChannel('plugins.flutter.io/cookie_manager'); - final List log = []; - - cookieChannel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - - if (methodCall.method == 'clearCookies') { - return true; - } - - // Return null explicitly instead of relying on the implicit null - // returned by the method channel if no return statement is specified. - return null; - }); - - tearDown(() { - log.clear(); - }); - - test('clearCookies should call `clearCookies` on the method channel', - () async { - await WKWebViewCookieManager().clearCookies(); - expect( - log, - [ - isMethodCall( - 'clearCookies', - arguments: null, - ), - ], - ); - }); - - test('setCookie should call `setCookie` on the method channel', () async { - await WKWebViewCookieManager().setCookie( - const WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev'), - ); - expect( - log, - [ - isMethodCall( - 'setCookie', - arguments: { - 'name': 'foo', - 'value': 'bar', - 'domain': 'flutter.dev', - 'path': '/', - }, - ), - ], - ); - }); -} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_navigation_delegate_test.dart new file mode 100644 index 000000000000..fe57e94364bf --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_navigation_delegate_test.dart @@ -0,0 +1,207 @@ +// 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/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; +import 'package:webview_flutter_wkwebview/src/v4/src/webkit_proxy.dart'; +import 'package:webview_flutter_wkwebview/src/v4/webview_flutter_wkwebview.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + group('WebKitNavigationDelegate', () { + test('setOnPageFinished', () { + final WebKitNavigationDelegate webKitDelgate = WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams( + webKitProxy: WebKitProxy( + createNavigationDelegate: CapturingNavigationDelegate.new, + ), + ), + ); + + late final String callbackUrl; + webKitDelgate.setOnPageFinished((String url) => callbackUrl = url); + + CapturingNavigationDelegate.lastCreatedDelegate.didFinishNavigation!( + WKWebView.detached(), + 'https://www.google.com', + ); + + expect(callbackUrl, 'https://www.google.com'); + }); + + test('setOnPageStarted', () { + final WebKitNavigationDelegate webKitDelgate = WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams( + webKitProxy: WebKitProxy( + createNavigationDelegate: CapturingNavigationDelegate.new, + ), + ), + ); + + late final String callbackUrl; + webKitDelgate.setOnPageStarted((String url) => callbackUrl = url); + + CapturingNavigationDelegate + .lastCreatedDelegate.didStartProvisionalNavigation!( + WKWebView.detached(), + 'https://www.google.com', + ); + + expect(callbackUrl, 'https://www.google.com'); + }); + + test('onWebResourceError from didFailNavigation', () { + final WebKitNavigationDelegate webKitDelgate = WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams( + webKitProxy: WebKitProxy( + createNavigationDelegate: CapturingNavigationDelegate.new, + ), + ), + ); + + late final WebKitWebResourceError callbackError; + void onWebResourceError(WebResourceError error) { + callbackError = error as WebKitWebResourceError; + } + + webKitDelgate.setOnWebResourceError(onWebResourceError); + + CapturingNavigationDelegate.lastCreatedDelegate.didFailNavigation!( + WKWebView.detached(), + const NSError( + code: WKErrorCode.webViewInvalidated, + domain: 'domain', + localizedDescription: 'my desc', + ), + ); + + expect(callbackError.description, 'my desc'); + expect(callbackError.errorCode, WKErrorCode.webViewInvalidated); + expect(callbackError.domain, 'domain'); + expect(callbackError.errorType, WebResourceErrorType.webViewInvalidated); + }); + + test('onWebResourceError from didFailProvisionalNavigation', () { + final WebKitNavigationDelegate webKitDelgate = WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams( + webKitProxy: WebKitProxy( + createNavigationDelegate: CapturingNavigationDelegate.new, + ), + ), + ); + + late final WebKitWebResourceError callbackError; + void onWebResourceError(WebResourceError error) { + callbackError = error as WebKitWebResourceError; + } + + webKitDelgate.setOnWebResourceError(onWebResourceError); + + CapturingNavigationDelegate + .lastCreatedDelegate.didFailProvisionalNavigation!( + WKWebView.detached(), + const NSError( + code: WKErrorCode.webViewInvalidated, + domain: 'domain', + localizedDescription: 'my desc', + ), + ); + + expect(callbackError.description, 'my desc'); + expect(callbackError.errorCode, WKErrorCode.webViewInvalidated); + expect(callbackError.domain, 'domain'); + expect(callbackError.errorType, WebResourceErrorType.webViewInvalidated); + }); + + test('onWebResourceError from webViewWebContentProcessDidTerminate', () { + final WebKitNavigationDelegate webKitDelgate = WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams( + webKitProxy: WebKitProxy( + createNavigationDelegate: CapturingNavigationDelegate.new, + ), + ), + ); + + late final WebKitWebResourceError callbackError; + void onWebResourceError(WebResourceError error) { + callbackError = error as WebKitWebResourceError; + } + + webKitDelgate.setOnWebResourceError(onWebResourceError); + + CapturingNavigationDelegate + .lastCreatedDelegate.webViewWebContentProcessDidTerminate!( + WKWebView.detached(), + ); + + expect(callbackError.description, ''); + expect(callbackError.errorCode, WKErrorCode.webContentProcessTerminated); + expect(callbackError.domain, 'WKErrorDomain'); + expect( + callbackError.errorType, + WebResourceErrorType.webContentProcessTerminated, + ); + }); + + test('onNavigationRequest from decidePolicyForNavigationAction', () { + final WebKitNavigationDelegate webKitDelgate = WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams( + webKitProxy: WebKitProxy( + createNavigationDelegate: CapturingNavigationDelegate.new, + ), + ), + ); + + late final String callbackUrl; + late final bool callbackIsMainFrame; + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) { + callbackUrl = url; + callbackIsMainFrame = isForMainFrame; + return true; + } + + webKitDelgate.setOnNavigationRequest(onNavigationRequest); + + expect( + CapturingNavigationDelegate + .lastCreatedDelegate.decidePolicyForNavigationAction!( + WKWebView.detached(), + const WKNavigationAction( + request: NSUrlRequest(url: 'https://www.google.com'), + targetFrame: WKFrameInfo(isMainFrame: false), + ), + ), + completion(WKNavigationActionPolicy.allow), + ); + + expect(callbackUrl, 'https://www.google.com'); + expect(callbackIsMainFrame, isFalse); + }); + }); +} + +// Records the last created instance of itself. +class CapturingNavigationDelegate extends WKNavigationDelegate { + CapturingNavigationDelegate({ + super.didFinishNavigation, + super.didStartProvisionalNavigation, + super.didFailNavigation, + super.didFailProvisionalNavigation, + super.decidePolicyForNavigationAction, + super.webViewWebContentProcessDidTerminate, + }) : super.detached() { + lastCreatedDelegate = this; + } + static CapturingNavigationDelegate lastCreatedDelegate = + CapturingNavigationDelegate(); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.dart new file mode 100644 index 000000000000..87a90db9a766 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.dart @@ -0,0 +1,844 @@ +// 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:math'; +// 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:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.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/v4/src/webkit_proxy.dart'; +import 'package:webview_flutter_wkwebview/src/v4/webview_flutter_wkwebview.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; + +import 'webkit_webview_controller_test.mocks.dart'; + +@GenerateMocks([ + UIScrollView, + WKPreferences, + WKUserContentController, + WKWebsiteDataStore, + WKWebView, + WKWebViewConfiguration, +]) +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + group('WebKitWebViewController', () { + WebKitWebViewController createControllerWithMocks({ + MockUIScrollView? mockScrollView, + MockWKPreferences? mockPreferences, + MockWKUserContentController? mockUserContentController, + MockWKWebsiteDataStore? mockWebsiteDataStore, + MockWKWebView Function( + WKWebViewConfiguration configuration, { + void Function( + String keyPath, + NSObject object, + Map change, + )? + observeValue, + })? + createMockWebView, + MockWKWebViewConfiguration? mockWebViewConfiguration, + }) { + final MockWKWebViewConfiguration nonNullMockWebViewConfiguration = + mockWebViewConfiguration ?? MockWKWebViewConfiguration(); + late final MockWKWebView nonNullMockWebView; + + final PlatformWebViewControllerCreationParams controllerCreationParams = + WebKitWebViewControllerCreationParams( + webKitProxy: WebKitProxy( + createWebViewConfiguration: () => nonNullMockWebViewConfiguration, + createWebView: ( + _, { + void Function( + String keyPath, + NSObject object, + Map change, + )? + observeValue, + }) { + nonNullMockWebView = createMockWebView == null + ? MockWKWebView() + : createMockWebView( + nonNullMockWebViewConfiguration, + observeValue: observeValue, + ); + return nonNullMockWebView; + }, + ), + ); + + final WebKitWebViewController controller = WebKitWebViewController( + controllerCreationParams, + ); + + when(nonNullMockWebView.scrollView) + .thenReturn(mockScrollView ?? MockUIScrollView()); + when(nonNullMockWebView.configuration) + .thenReturn(nonNullMockWebViewConfiguration); + + when(nonNullMockWebViewConfiguration.preferences) + .thenReturn(mockPreferences ?? MockWKPreferences()); + when(nonNullMockWebViewConfiguration.userContentController).thenReturn( + mockUserContentController ?? MockWKUserContentController()); + when(nonNullMockWebViewConfiguration.websiteDataStore) + .thenReturn(mockWebsiteDataStore ?? MockWKWebsiteDataStore()); + + return controller; + } + + test('loadFile', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + await controller.loadFile('/path/to/file.html'); + verify(mockWebView.loadFileUrl( + '/path/to/file.html', + readAccessUrl: '/path/to', + )); + }); + + test('loadFlutterAsset', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + await controller.loadFlutterAsset('test_assets/index.html'); + verify(mockWebView.loadFlutterAsset('test_assets/index.html')); + }); + + test('loadHtmlString', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + const String htmlString = 'Test data.'; + await controller.loadHtmlString(htmlString, baseUrl: 'baseUrl'); + + verify(mockWebView.loadHtmlString( + 'Test data.', + baseUrl: 'baseUrl', + )); + }); + + group('loadRequest', () { + test('Throws ArgumentError for empty scheme', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + expect( + () async => await controller.loadRequest( + LoadRequestParams( + uri: Uri.parse('www.google.com'), + method: LoadRequestMethod.get, + headers: const {}, + ), + ), + throwsA(isA()), + ); + }); + + test('GET without headers', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + await controller.loadRequest( + LoadRequestParams( + uri: Uri.parse('https://www.google.com'), + method: LoadRequestMethod.get, + headers: const {}, + ), + ); + + final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny)) + .captured + .single as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.allHttpHeaderFields, {}); + expect(request.httpMethod, 'get'); + }); + + test('GET with headers', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + await controller.loadRequest( + LoadRequestParams( + uri: Uri.parse('https://www.google.com'), + method: LoadRequestMethod.get, + headers: const {'a': 'header'}, + ), + ); + + final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny)) + .captured + .single as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.allHttpHeaderFields, {'a': 'header'}); + expect(request.httpMethod, 'get'); + }); + + test('POST without body', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + await controller.loadRequest(LoadRequestParams( + uri: Uri.parse('https://www.google.com'), + method: LoadRequestMethod.post, + headers: const {}, + )); + + final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny)) + .captured + .single as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.httpMethod, 'post'); + }); + + test('POST with body', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + await controller.loadRequest(LoadRequestParams( + uri: Uri.parse('https://www.google.com'), + method: LoadRequestMethod.post, + body: Uint8List.fromList('Test Body'.codeUnits), + headers: const {}, + )); + + final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny)) + .captured + .single as NSUrlRequest; + expect(request.url, 'https://www.google.com'); + expect(request.httpMethod, 'post'); + expect( + request.httpBody, + Uint8List.fromList('Test Body'.codeUnits), + ); + }); + }); + + test('canGoBack', () { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + when(mockWebView.canGoBack()).thenAnswer( + (_) => Future.value(false), + ); + expect(controller.canGoBack(), completion(false)); + }); + + test('canGoForward', () { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + when(mockWebView.canGoForward()).thenAnswer( + (_) => Future.value(true), + ); + expect(controller.canGoForward(), completion(true)); + }); + + test('goBack', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + await controller.goBack(); + verify(mockWebView.goBack()); + }); + + test('goForward', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + await controller.goForward(); + verify(mockWebView.goForward()); + }); + + test('reload', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + await controller.reload(); + verify(mockWebView.reload()); + }); + + test('enableGestureNavigation', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + await controller.enableGestureNavigation(true); + verify(mockWebView.setAllowsBackForwardNavigationGestures(true)); + }); + + test('runJavaScriptReturningResult', () { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value('returnString'), + ); + expect( + controller.runJavaScriptReturningResult('runJavaScript'), + completion('returnString'), + ); + }); + + test('runJavaScriptReturningResult throws error on null return value', () { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value(), + ); + expect( + () => controller.runJavaScriptReturningResult('runJavaScript'), + throwsArgumentError, + ); + }); + + test('runJavaScript', () { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( + (_) => Future.value('returnString'), + ); + expect( + controller.runJavaScript('runJavaScript'), + completes, + ); + }); + + test('runJavaScript ignores exception with unsupported javaScript type', + () { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + when(mockWebView.evaluateJavaScript('runJavaScript')) + .thenThrow(PlatformException( + code: '', + details: const NSError( + code: WKErrorCode.javaScriptResultTypeIsUnsupported, + domain: '', + localizedDescription: '', + ), + )); + expect( + controller.runJavaScript('runJavaScript'), + completes, + ); + }); + + test('getTitle', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + when(mockWebView.getTitle()) + .thenAnswer((_) => Future.value('Web Title')); + expect(controller.getTitle(), completion('Web Title')); + }); + + test('currentUrl', () { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + when(mockWebView.getUrl()) + .thenAnswer((_) => Future.value('myUrl.com')); + expect(controller.currentUrl(), completion('myUrl.com')); + }); + + test('scrollTo', () async { + final MockUIScrollView mockScrollView = MockUIScrollView(); + + final WebKitWebViewController controller = createControllerWithMocks( + mockScrollView: mockScrollView, + ); + + await controller.scrollTo(2, 4); + verify(mockScrollView.setContentOffset(const Point(2.0, 4.0))); + }); + + test('scrollBy', () async { + final MockUIScrollView mockScrollView = MockUIScrollView(); + + final WebKitWebViewController controller = createControllerWithMocks( + mockScrollView: mockScrollView, + ); + + await controller.scrollBy(2, 4); + verify(mockScrollView.scrollBy(const Point(2.0, 4.0))); + }); + + test('getScrollPosition', () { + final MockUIScrollView mockScrollView = MockUIScrollView(); + + final WebKitWebViewController controller = createControllerWithMocks( + mockScrollView: mockScrollView, + ); + + when(mockScrollView.getContentOffset()).thenAnswer( + (_) => Future>.value(const Point(8.0, 16.0)), + ); + expect( + controller.getScrollPosition(), + completion(const Point(8.0, 16.0)), + ); + }); + + test('disable zoom', () async { + final MockWKUserContentController mockUserContentController = + MockWKUserContentController(); + + final WebKitWebViewController controller = createControllerWithMocks( + mockUserContentController: mockUserContentController, + ); + + await controller.enableZoom(false); + + final WKUserScript zoomScript = + verify(mockUserContentController.addUserScript(captureAny)) + .captured + .first as WKUserScript; + expect(zoomScript.isMainFrameOnly, isTrue); + expect(zoomScript.injectionTime, WKUserScriptInjectionTime.atDocumentEnd); + expect( + zoomScript.source, + "var meta = document.createElement('meta');\n" + "meta.name = 'viewport';\n" + "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, " + "user-scalable=no';\n" + "var head = document.getElementsByTagName('head')[0];head.appendChild(meta);", + ); + }); + + test('setBackgroundColor', () async { + final MockWKWebView mockWebView = MockWKWebView(); + final MockUIScrollView mockScrollView = MockUIScrollView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + mockScrollView: mockScrollView, + ); + + controller.setBackgroundColor(Colors.red); + + verify(mockWebView.setOpaque(false)); + verify(mockWebView.setBackgroundColor(Colors.transparent)); + verify(mockScrollView.setBackgroundColor(Colors.red)); + }); + + test('userAgent', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + await controller.setUserAgent('MyUserAgent'); + verify(mockWebView.setCustomUserAgent('MyUserAgent')); + }); + + test('enable JavaScript', () async { + final MockWKPreferences mockPreferences = MockWKPreferences(); + + final WebKitWebViewController controller = createControllerWithMocks( + mockPreferences: mockPreferences, + ); + + await controller.setJavaScriptMode(JavaScriptMode.unrestricted); + + verify(mockPreferences.setJavaScriptEnabled(true)); + }); + + test('disable JavaScript', () async { + final MockWKPreferences mockPreferences = MockWKPreferences(); + + final WebKitWebViewController controller = createControllerWithMocks( + mockPreferences: mockPreferences, + ); + + await controller.setJavaScriptMode(JavaScriptMode.disabled); + + verify(mockPreferences.setJavaScriptEnabled(false)); + }); + + test('clearCache', () { + final MockWKWebsiteDataStore mockWebsiteDataStore = + MockWKWebsiteDataStore(); + + final WebKitWebViewController controller = createControllerWithMocks( + mockWebsiteDataStore: mockWebsiteDataStore, + ); + when( + mockWebsiteDataStore.removeDataOfTypes( + { + WKWebsiteDataType.memoryCache, + WKWebsiteDataType.diskCache, + WKWebsiteDataType.offlineWebApplicationCache, + }, + DateTime.fromMillisecondsSinceEpoch(0), + ), + ).thenAnswer((_) => Future.value(false)); + + expect(controller.clearCache(), completes); + }); + + test('clearLocalStorage', () { + final MockWKWebsiteDataStore mockWebsiteDataStore = + MockWKWebsiteDataStore(); + + final WebKitWebViewController controller = createControllerWithMocks( + mockWebsiteDataStore: mockWebsiteDataStore, + ); + when( + mockWebsiteDataStore.removeDataOfTypes( + {WKWebsiteDataType.localStorage}, + DateTime.fromMillisecondsSinceEpoch(0), + ), + ).thenAnswer((_) => Future.value(false)); + + expect(controller.clearLocalStorage(), completes); + }); + + test('addJavaScriptChannel', () async { + final WebKitProxy webKitProxy = WebKitProxy( + createScriptMessageHandler: ({ + required void Function( + WKUserContentController userContentController, + WKScriptMessage message, + ) + didReceiveScriptMessage, + }) { + return WKScriptMessageHandler.detached( + didReceiveScriptMessage: didReceiveScriptMessage, + ); + }, + ); + + final WebKitJavaScriptChannelParams javaScriptChannelParams = + WebKitJavaScriptChannelParams( + name: 'name', + onMessageReceived: (JavaScriptMessage message) {}, + webKitProxy: webKitProxy, + ); + + final MockWKUserContentController mockUserContentController = + MockWKUserContentController(); + + final WebKitWebViewController controller = createControllerWithMocks( + mockUserContentController: mockUserContentController, + ); + + await controller.addJavaScriptChannel(javaScriptChannelParams); + verify(mockUserContentController.addScriptMessageHandler( + argThat(isA()), + 'name', + )); + + final WKUserScript userScript = + verify(mockUserContentController.addUserScript(captureAny)) + .captured + .single as WKUserScript; + expect(userScript.source, 'window.name = webkit.messageHandlers.name;'); + expect( + userScript.injectionTime, + WKUserScriptInjectionTime.atDocumentStart, + ); + }); + + test('removeJavaScriptChannel', () async { + final WebKitProxy webKitProxy = WebKitProxy( + createScriptMessageHandler: ({ + required void Function( + WKUserContentController userContentController, + WKScriptMessage message, + ) + didReceiveScriptMessage, + }) { + return WKScriptMessageHandler.detached( + didReceiveScriptMessage: didReceiveScriptMessage, + ); + }, + ); + + final WebKitJavaScriptChannelParams javaScriptChannelParams = + WebKitJavaScriptChannelParams( + name: 'name', + onMessageReceived: (JavaScriptMessage message) {}, + webKitProxy: webKitProxy, + ); + + final MockWKUserContentController mockUserContentController = + MockWKUserContentController(); + + final WebKitWebViewController controller = createControllerWithMocks( + mockUserContentController: mockUserContentController, + ); + + await controller.addJavaScriptChannel(javaScriptChannelParams); + reset(mockUserContentController); + + await controller.removeJavaScriptChannel('name'); + + verify(mockUserContentController.removeAllUserScripts()); + verify(mockUserContentController.removeScriptMessageHandler('name')); + + verifyNoMoreInteractions(mockUserContentController); + }); + + test('removeJavaScriptChannel with zoom disabled', () async { + final WebKitProxy webKitProxy = WebKitProxy( + createScriptMessageHandler: ({ + required void Function( + WKUserContentController userContentController, + WKScriptMessage message, + ) + didReceiveScriptMessage, + }) { + return WKScriptMessageHandler.detached( + didReceiveScriptMessage: didReceiveScriptMessage, + ); + }, + ); + + final WebKitJavaScriptChannelParams javaScriptChannelParams = + WebKitJavaScriptChannelParams( + name: 'name', + onMessageReceived: (JavaScriptMessage message) {}, + webKitProxy: webKitProxy, + ); + + final MockWKUserContentController mockUserContentController = + MockWKUserContentController(); + + final WebKitWebViewController controller = createControllerWithMocks( + mockUserContentController: mockUserContentController, + ); + + await controller.enableZoom(false); + await controller.addJavaScriptChannel(javaScriptChannelParams); + clearInteractions(mockUserContentController); + await controller.removeJavaScriptChannel('name'); + + final WKUserScript zoomScript = + verify(mockUserContentController.addUserScript(captureAny)) + .captured + .first as WKUserScript; + expect(zoomScript.isMainFrameOnly, isTrue); + expect(zoomScript.injectionTime, WKUserScriptInjectionTime.atDocumentEnd); + expect( + zoomScript.source, + "var meta = document.createElement('meta');\n" + "meta.name = 'viewport';\n" + "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, " + "user-scalable=no';\n" + "var head = document.getElementsByTagName('head')[0];head.appendChild(meta);", + ); + }); + + test('setPlatformNavigationDelegate', () { + final MockWKWebView mockWebView = MockWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + final WebKitNavigationDelegate navigationDelegate = + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams( + webKitProxy: WebKitProxy( + createNavigationDelegate: CapturingNavigationDelegate.new, + ), + ), + ); + + controller.setPlatformNavigationDelegate(navigationDelegate); + + verify( + mockWebView.setNavigationDelegate( + CapturingNavigationDelegate.lastCreatedDelegate, + ), + ); + }); + + test('setPlatformNavigationDelegate onProgress', () async { + final MockWKWebView mockWebView = MockWKWebView(); + + late final void Function( + String keyPath, + NSObject object, + Map change, + ) webViewObserveValue; + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: ( + _, { + void Function( + String keyPath, + NSObject object, + Map change, + )? + observeValue, + }) { + webViewObserveValue = observeValue!; + return mockWebView; + }, + ); + + verify( + mockWebView.addObserver( + mockWebView, + keyPath: 'estimatedProgress', + options: { + NSKeyValueObservingOptions.newValue, + }, + ), + ); + + final WebKitNavigationDelegate navigationDelegate = + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams( + webKitProxy: WebKitProxy( + createNavigationDelegate: CapturingNavigationDelegate.new, + ), + ), + ); + + late final int callbackProgress; + navigationDelegate.setOnProgress( + (int progress) => callbackProgress = progress, + ); + + await controller.setPlatformNavigationDelegate(navigationDelegate); + + webViewObserveValue( + 'estimatedProgress', + mockWebView, + {NSKeyValueChangeKey.newValue: 0.0}, + ); + + expect(callbackProgress, 0); + }); + }); + + group('WebKitJavaScriptChannelParams', () { + test('onMessageReceived', () async { + late final WKScriptMessageHandler messageHandler; + + final WebKitProxy webKitProxy = WebKitProxy( + createScriptMessageHandler: ({ + required void Function( + WKUserContentController userContentController, + WKScriptMessage message, + ) + didReceiveScriptMessage, + }) { + messageHandler = WKScriptMessageHandler.detached( + didReceiveScriptMessage: didReceiveScriptMessage, + ); + return messageHandler; + }, + ); + + late final String callbackMessage; + WebKitJavaScriptChannelParams( + name: 'name', + onMessageReceived: (JavaScriptMessage message) { + callbackMessage = message.message; + }, + webKitProxy: webKitProxy, + ); + + messageHandler.didReceiveScriptMessage( + MockWKUserContentController(), + const WKScriptMessage(name: 'name', body: 'myMessage'), + ); + + expect(callbackMessage, 'myMessage'); + }); + }); +} + +// Records the last created instance of itself. +class CapturingNavigationDelegate extends WKNavigationDelegate { + CapturingNavigationDelegate({ + super.didFinishNavigation, + super.didStartProvisionalNavigation, + super.didFailNavigation, + super.didFailProvisionalNavigation, + super.decidePolicyForNavigationAction, + super.webViewWebContentProcessDidTerminate, + }) : super.detached() { + lastCreatedDelegate = this; + } + static CapturingNavigationDelegate lastCreatedDelegate = + CapturingNavigationDelegate(); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.mocks.dart new file mode 100644 index 000000000000..17d47917b2ee --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.mocks.dart @@ -0,0 +1,414 @@ +// Mocks generated by Mockito 5.2.0 from annotations +// in webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i5; +import 'dart:math' as _i2; +import 'dart:ui' as _i6; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' + as _i7; +import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart' as _i3; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i4; + +// 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 + +class _FakePoint_0 extends _i1.Fake implements _i2.Point {} + +class _FakeUIScrollView_1 extends _i1.Fake implements _i3.UIScrollView {} + +class _FakeWKPreferences_2 extends _i1.Fake implements _i4.WKPreferences {} + +class _FakeWKUserContentController_3 extends _i1.Fake + implements _i4.WKUserContentController {} + +class _FakeWKHttpCookieStore_4 extends _i1.Fake + implements _i4.WKHttpCookieStore {} + +class _FakeWKWebsiteDataStore_5 extends _i1.Fake + implements _i4.WKWebsiteDataStore {} + +class _FakeWKWebViewConfiguration_6 extends _i1.Fake + implements _i4.WKWebViewConfiguration {} + +class _FakeWKWebView_7 extends _i1.Fake implements _i4.WKWebView {} + +/// A class which mocks [UIScrollView]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockUIScrollView extends _i1.Mock implements _i3.UIScrollView { + MockUIScrollView() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future<_i2.Point> getContentOffset() => (super.noSuchMethod( + Invocation.method(#getContentOffset, []), + returnValue: Future<_i2.Point>.value(_FakePoint_0())) + as _i5.Future<_i2.Point>); + @override + _i5.Future scrollBy(_i2.Point? offset) => + (super.noSuchMethod(Invocation.method(#scrollBy, [offset]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setContentOffset(_i2.Point? offset) => + (super.noSuchMethod(Invocation.method(#setContentOffset, [offset]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i3.UIScrollView copy() => (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeUIScrollView_1()) as _i3.UIScrollView); + @override + _i5.Future setBackgroundColor(_i6.Color? color) => + (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setOpaque(bool? opaque) => + (super.noSuchMethod(Invocation.method(#setOpaque, [opaque]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); +} + +/// A class which mocks [WKPreferences]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKPreferences extends _i1.Mock implements _i4.WKPreferences { + MockWKPreferences() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future setJavaScriptEnabled(bool? enabled) => + (super.noSuchMethod(Invocation.method(#setJavaScriptEnabled, [enabled]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i4.WKPreferences copy() => (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKPreferences_2()) as _i4.WKPreferences); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); +} + +/// A class which mocks [WKUserContentController]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKUserContentController extends _i1.Mock + implements _i4.WKUserContentController { + MockWKUserContentController() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future addScriptMessageHandler( + _i4.WKScriptMessageHandler? handler, String? name) => + (super.noSuchMethod( + Invocation.method(#addScriptMessageHandler, [handler, name]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeScriptMessageHandler(String? name) => (super + .noSuchMethod(Invocation.method(#removeScriptMessageHandler, [name]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeAllScriptMessageHandlers() => (super.noSuchMethod( + Invocation.method(#removeAllScriptMessageHandlers, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future addUserScript(_i4.WKUserScript? userScript) => + (super.noSuchMethod(Invocation.method(#addUserScript, [userScript]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeAllUserScripts() => + (super.noSuchMethod(Invocation.method(#removeAllUserScripts, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i4.WKUserContentController copy() => + (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKUserContentController_3()) + as _i4.WKUserContentController); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); +} + +/// A class which mocks [WKWebsiteDataStore]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebsiteDataStore extends _i1.Mock + implements _i4.WKWebsiteDataStore { + MockWKWebsiteDataStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKHttpCookieStore get httpCookieStore => + (super.noSuchMethod(Invocation.getter(#httpCookieStore), + returnValue: _FakeWKHttpCookieStore_4()) as _i4.WKHttpCookieStore); + @override + _i5.Future removeDataOfTypes( + Set<_i4.WKWebsiteDataType>? dataTypes, DateTime? since) => + (super.noSuchMethod( + Invocation.method(#removeDataOfTypes, [dataTypes, since]), + returnValue: Future.value(false)) as _i5.Future); + @override + _i4.WKWebsiteDataStore copy() => + (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKWebsiteDataStore_5()) as _i4.WKWebsiteDataStore); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); +} + +/// A class which mocks [WKWebView]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebView extends _i1.Mock implements _i4.WKWebView { + MockWKWebView() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKWebViewConfiguration get configuration => + (super.noSuchMethod(Invocation.getter(#configuration), + returnValue: _FakeWKWebViewConfiguration_6()) + as _i4.WKWebViewConfiguration); + @override + _i3.UIScrollView get scrollView => + (super.noSuchMethod(Invocation.getter(#scrollView), + returnValue: _FakeUIScrollView_1()) as _i3.UIScrollView); + @override + _i5.Future setUIDelegate(_i4.WKUIDelegate? delegate) => + (super.noSuchMethod(Invocation.method(#setUIDelegate, [delegate]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setNavigationDelegate(_i4.WKNavigationDelegate? delegate) => + (super.noSuchMethod(Invocation.method(#setNavigationDelegate, [delegate]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future getUrl() => + (super.noSuchMethod(Invocation.method(#getUrl, []), + returnValue: Future.value()) as _i5.Future); + @override + _i5.Future getEstimatedProgress() => + (super.noSuchMethod(Invocation.method(#getEstimatedProgress, []), + returnValue: Future.value(0.0)) as _i5.Future); + @override + _i5.Future loadRequest(_i7.NSUrlRequest? request) => + (super.noSuchMethod(Invocation.method(#loadRequest, [request]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future loadHtmlString(String? string, {String? baseUrl}) => + (super.noSuchMethod( + Invocation.method(#loadHtmlString, [string], {#baseUrl: baseUrl}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future loadFileUrl(String? url, {String? readAccessUrl}) => + (super.noSuchMethod( + Invocation.method( + #loadFileUrl, [url], {#readAccessUrl: readAccessUrl}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future loadFlutterAsset(String? key) => + (super.noSuchMethod(Invocation.method(#loadFlutterAsset, [key]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future canGoBack() => + (super.noSuchMethod(Invocation.method(#canGoBack, []), + returnValue: Future.value(false)) as _i5.Future); + @override + _i5.Future canGoForward() => + (super.noSuchMethod(Invocation.method(#canGoForward, []), + returnValue: Future.value(false)) as _i5.Future); + @override + _i5.Future goBack() => + (super.noSuchMethod(Invocation.method(#goBack, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future goForward() => + (super.noSuchMethod(Invocation.method(#goForward, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future reload() => + (super.noSuchMethod(Invocation.method(#reload, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future getTitle() => + (super.noSuchMethod(Invocation.method(#getTitle, []), + returnValue: Future.value()) as _i5.Future); + @override + _i5.Future setAllowsBackForwardNavigationGestures(bool? allow) => + (super.noSuchMethod( + Invocation.method(#setAllowsBackForwardNavigationGestures, [allow]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setCustomUserAgent(String? userAgent) => + (super.noSuchMethod(Invocation.method(#setCustomUserAgent, [userAgent]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future evaluateJavaScript(String? javaScriptString) => (super + .noSuchMethod(Invocation.method(#evaluateJavaScript, [javaScriptString]), + returnValue: Future.value()) as _i5.Future); + @override + _i4.WKWebView copy() => (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKWebView_7()) as _i4.WKWebView); + @override + _i5.Future setBackgroundColor(_i6.Color? color) => + (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setOpaque(bool? opaque) => + (super.noSuchMethod(Invocation.method(#setOpaque, [opaque]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); +} + +/// A class which mocks [WKWebViewConfiguration]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebViewConfiguration extends _i1.Mock + implements _i4.WKWebViewConfiguration { + MockWKWebViewConfiguration() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKUserContentController get userContentController => + (super.noSuchMethod(Invocation.getter(#userContentController), + returnValue: _FakeWKUserContentController_3()) + as _i4.WKUserContentController); + @override + _i4.WKPreferences get preferences => + (super.noSuchMethod(Invocation.getter(#preferences), + returnValue: _FakeWKPreferences_2()) as _i4.WKPreferences); + @override + _i4.WKWebsiteDataStore get websiteDataStore => + (super.noSuchMethod(Invocation.getter(#websiteDataStore), + returnValue: _FakeWKWebsiteDataStore_5()) as _i4.WKWebsiteDataStore); + @override + _i5.Future setAllowsInlineMediaPlayback(bool? allow) => (super + .noSuchMethod(Invocation.method(#setAllowsInlineMediaPlayback, [allow]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future setMediaTypesRequiringUserActionForPlayback( + Set<_i4.WKAudiovisualMediaType>? types) => + (super.noSuchMethod( + Invocation.method( + #setMediaTypesRequiringUserActionForPlayback, [types]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i4.WKWebViewConfiguration copy() => + (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKWebViewConfiguration_6()) + as _i4.WKWebViewConfiguration); + @override + _i5.Future addObserver(_i7.NSObject? observer, + {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.dart new file mode 100644 index 000000000000..71107cb563a6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.dart @@ -0,0 +1,112 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; +import 'package:webview_flutter_wkwebview/src/v4/src/webkit_proxy.dart'; +import 'package:webview_flutter_wkwebview/src/v4/webview_flutter_wkwebview.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; + +import 'webkit_webview_cookie_manager_test.mocks.dart'; + +@GenerateMocks([WKWebsiteDataStore, WKHttpCookieStore]) +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + group('WebKitWebViewCookieManager', () { + test('clearCookies', () { + final MockWKWebsiteDataStore mockWKWebsiteDataStore = + MockWKWebsiteDataStore(); + + final WebKitWebViewCookieManager manager = WebKitWebViewCookieManager( + WebKitWebViewCookieManagerCreationParams( + webKitProxy: WebKitProxy( + defaultWebsiteDataStore: () => mockWKWebsiteDataStore, + ), + ), + ); + + when( + mockWKWebsiteDataStore.removeDataOfTypes( + {WKWebsiteDataType.cookies}, + any, + ), + ).thenAnswer((_) => Future.value(true)); + expect(manager.clearCookies(), completion(true)); + + when( + mockWKWebsiteDataStore.removeDataOfTypes( + {WKWebsiteDataType.cookies}, + any, + ), + ).thenAnswer((_) => Future.value(false)); + expect(manager.clearCookies(), completion(false)); + }); + + test('setCookie', () async { + final MockWKWebsiteDataStore mockWKWebsiteDataStore = + MockWKWebsiteDataStore(); + + final MockWKHttpCookieStore mockCookieStore = MockWKHttpCookieStore(); + when(mockWKWebsiteDataStore.httpCookieStore).thenReturn(mockCookieStore); + + final WebKitWebViewCookieManager manager = WebKitWebViewCookieManager( + WebKitWebViewCookieManagerCreationParams( + webKitProxy: WebKitProxy( + defaultWebsiteDataStore: () => mockWKWebsiteDataStore, + ), + ), + ); + + await manager.setCookie( + const WebViewCookie(name: 'a', value: 'b', domain: 'c', path: 'd'), + ); + + final NSHttpCookie cookie = verify(mockCookieStore.setCookie(captureAny)) + .captured + .single as NSHttpCookie; + expect( + cookie.properties, + { + NSHttpCookiePropertyKey.name: 'a', + NSHttpCookiePropertyKey.value: 'b', + NSHttpCookiePropertyKey.domain: 'c', + NSHttpCookiePropertyKey.path: 'd', + }, + ); + }); + + test('setCookie throws argument error with invalid path', () async { + final MockWKWebsiteDataStore mockWKWebsiteDataStore = + MockWKWebsiteDataStore(); + + final MockWKHttpCookieStore mockCookieStore = MockWKHttpCookieStore(); + when(mockWKWebsiteDataStore.httpCookieStore).thenReturn(mockCookieStore); + + final WebKitWebViewCookieManager manager = WebKitWebViewCookieManager( + WebKitWebViewCookieManagerCreationParams( + webKitProxy: WebKitProxy( + defaultWebsiteDataStore: () => mockWKWebsiteDataStore, + ), + ), + ); + + expect( + () => manager.setCookie( + WebViewCookie( + name: 'a', + value: 'b', + domain: 'c', + path: String.fromCharCode(0x1F), + ), + ), + throwsArgumentError, + ); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.mocks.dart new file mode 100644 index 000000000000..90bf2522ab77 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.mocks.dart @@ -0,0 +1,100 @@ +// Mocks generated by Mockito 5.2.0 from annotations +// in webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' + as _i4; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.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 + +class _FakeWKHttpCookieStore_0 extends _i1.Fake + implements _i2.WKHttpCookieStore {} + +class _FakeWKWebsiteDataStore_1 extends _i1.Fake + implements _i2.WKWebsiteDataStore {} + +/// A class which mocks [WKWebsiteDataStore]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebsiteDataStore extends _i1.Mock + implements _i2.WKWebsiteDataStore { + MockWKWebsiteDataStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WKHttpCookieStore get httpCookieStore => + (super.noSuchMethod(Invocation.getter(#httpCookieStore), + returnValue: _FakeWKHttpCookieStore_0()) as _i2.WKHttpCookieStore); + @override + _i3.Future removeDataOfTypes( + Set<_i2.WKWebsiteDataType>? dataTypes, DateTime? since) => + (super.noSuchMethod( + Invocation.method(#removeDataOfTypes, [dataTypes, since]), + returnValue: Future.value(false)) as _i3.Future); + @override + _i2.WKWebsiteDataStore copy() => + (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKWebsiteDataStore_1()) as _i2.WKWebsiteDataStore); + @override + _i3.Future addObserver(_i4.NSObject? observer, + {String? keyPath, Set<_i4.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); + @override + _i3.Future removeObserver(_i4.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); +} + +/// A class which mocks [WKHttpCookieStore]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKHttpCookieStore extends _i1.Mock implements _i2.WKHttpCookieStore { + MockWKHttpCookieStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future setCookie(_i4.NSHttpCookie? cookie) => + (super.noSuchMethod(Invocation.method(#setCookie, [cookie]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); + @override + _i2.WKHttpCookieStore copy() => + (super.noSuchMethod(Invocation.method(#copy, []), + returnValue: _FakeWKHttpCookieStore_0()) as _i2.WKHttpCookieStore); + @override + _i3.Future addObserver(_i4.NSObject? observer, + {String? keyPath, Set<_i4.NSKeyValueObservingOptions>? options}) => + (super.noSuchMethod( + Invocation.method( + #addObserver, [observer], {#keyPath: keyPath, #options: options}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); + @override + _i3.Future removeObserver(_i4.NSObject? observer, {String? keyPath}) => + (super.noSuchMethod( + Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_widget_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_widget_test.dart new file mode 100644 index 000000000000..36a95d6f1cab --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_widget_test.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. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.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/v4/src/webkit_proxy.dart'; +import 'package:webview_flutter_wkwebview/src/v4/webview_flutter_wkwebview.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('WebKitWebViewWidget', () { + testWidgets('build', (WidgetTester tester) async { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final WebKitWebViewController controller = WebKitWebViewController( + WebKitWebViewControllerCreationParams( + webKitProxy: WebKitProxy( + createWebView: ( + WKWebViewConfiguration configuration, { + void Function( + String keyPath, + NSObject object, + Map change, + )? + observeValue, + }) { + final WKWebView webView = WKWebView.detached( + instanceManager: instanceManager, + ); + instanceManager.addDartCreatedInstance(webView); + return webView; + }, + createWebViewConfiguration: WKWebViewConfiguration.detached, + ), + ), + ); + + final WebKitWebViewWidget widget = WebKitWebViewWidget( + WebKitWebViewWidgetCreationParams( + controller: controller, + instanceManager: instanceManager, + ), + ); + + await tester.pumpWidget( + Builder(builder: (BuildContext context) => widget.build(context)), + ); + + expect(find.byType(UiKitView), findsOneWidget); + }); + }); +} diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index 23b7f7bbc35b..f735019d61c4 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -1,15 +1,12 @@ # Plugins that deliberately use their own analysis_options.yaml. # -# This only exists to allow incrementally switching to the newer, stricter -# analysis_options.yaml based on flutter/flutter, rather than the original -# rules based on pedantic (now at analysis_options_legacy.yaml). +# This only exists to allow incrementally adopting new analysis options in +# cases where a new option can't be applied to the entire repository at +# once. Do not add anything to this file without an issue reference and +# a concrete plan for removing it relatively quickly. # # DO NOT move or delete this file without updating # https://github.com/dart-lang/sdk/blob/master/tools/bots/flutter/analyze_flutter_plugins.sh # which references this file from source, but out-of-repo. # Contact stuartmorgan or devoncarew for assistance if necessary. -# TODO(ecosystem): Remove everything from this list. See: -# https://github.com/flutter/flutter/issues/76229 -- google_maps_flutter/google_maps_flutter_platform_interface -- google_maps_flutter/google_maps_flutter_web diff --git a/script/configs/exclude_integration_ios.yaml b/script/configs/exclude_integration_ios.yaml index 06283ae59c16..2d535cd4f0dc 100644 --- a/script/configs/exclude_integration_ios.yaml +++ b/script/configs/exclude_integration_ios.yaml @@ -1,4 +1,5 @@ -# Currently missing: https://github.com/flutter/flutter/issues/81695 -- in_app_purchase_storekit # Currently missing: https://github.com/flutter/flutter/issues/82208 - ios_platform_images +# Can't use Flutter integration tests due to native modal UI. +- file_selector_ios +- file_selector \ No newline at end of file diff --git a/script/configs/exclude_integration_linux.yaml b/script/configs/exclude_integration_linux.yaml new file mode 100644 index 000000000000..a83550e6808f --- /dev/null +++ b/script/configs/exclude_integration_linux.yaml @@ -0,0 +1,3 @@ +# Can't use Flutter integration tests due to native modal UI. +- file_selector +- file_selector_linux diff --git a/script/configs/exclude_integration_macos.yaml b/script/configs/exclude_integration_macos.yaml new file mode 100644 index 000000000000..7a9e287da05f --- /dev/null +++ b/script/configs/exclude_integration_macos.yaml @@ -0,0 +1,3 @@ +# Can't use Flutter integration tests due to native modal UI. +- file_selector +- file_selector_macos diff --git a/script/configs/exclude_integration_win32.yaml b/script/configs/exclude_integration_win32.yaml index 4626fbd79ce7..09306691e5ed 100644 --- a/script/configs/exclude_integration_win32.yaml +++ b/script/configs/exclude_integration_win32.yaml @@ -1,3 +1,4 @@ # Can't use Flutter integration tests due to native modal UI. - file_selector - file_selector_windows +- image_picker_windows \ No newline at end of file diff --git a/script/configs/temp_exclude_excerpt.yaml b/script/configs/temp_exclude_excerpt.yaml new file mode 100644 index 000000000000..c59983efd058 --- /dev/null +++ b/script/configs/temp_exclude_excerpt.yaml @@ -0,0 +1,23 @@ +# Packages that have not yet adopted code-excerpt. +# +# This only exists to allow incrementally adopting the new requirement. +# Packages shoud never be added to this list. + +# TODO(ecosystem): Remove everything from this list. See +# https://github.com/flutter/flutter/issues/102679 +- camera_web +- espresso +- google_maps_flutter/google_maps_flutter +- google_sign_in/google_sign_in +- google_sign_in_web +- image_picker/image_picker +- image_picker_for_web +- in_app_purchase/in_app_purchase +- ios_platform_images +- path_provider/path_provider +- plugin_platform_interface +- quick_actions/quick_actions +- shared_preferences/shared_preferences +- webview_flutter/webview_flutter +- webview_flutter_android +- webview_flutter_web diff --git a/script/install_chromium.sh b/script/install_chromium.sh index 1cb38af05496..0d360fe98cfe 100755 --- a/script/install_chromium.sh +++ b/script/install_chromium.sh @@ -10,24 +10,35 @@ readonly TARGET_DIR=$1 # The build of Chromium used to test web functionality. # # Chromium builds can be located here: https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/ -readonly CHROMIUM_BUILD=768968 -# The ChromeDriver version corresponding to the build above. See -# https://chromedriver.chromium.org/downloads -# for versions mappings when updating Chromium. -readonly CHROME_DRIVER_VERSION=84.0.4147.30 +# +# Check: https://github.com/flutter/engine/blob/main/lib/web_ui/dev/browser_lock.yaml +readonly CHROMIUM_BUILD=929514 + +# The correct ChromeDriver is distributed alongside the chromium build above, as +# `chromedriver_linux64.zip`, so no need to hardcode any extra info about it. +readonly DOWNLOAD_ROOT="https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F${CHROMIUM_BUILD}%2F" # Install Chromium. mkdir "$TARGET_DIR" -wget --no-verbose "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F${CHROMIUM_BUILD}%2Fchrome-linux.zip?alt=media" -O "$TARGET_DIR"/chromium.zip -unzip "$TARGET_DIR"/chromium.zip -d "$TARGET_DIR"/ +readonly CHROMIUM_ZIP_FILE="$TARGET_DIR/chromium.zip" +wget --no-verbose "${DOWNLOAD_ROOT}chrome-linux.zip?alt=media" -O "$CHROMIUM_ZIP_FILE" +unzip -q "$CHROMIUM_ZIP_FILE" -d "$TARGET_DIR/" # Install ChromeDriver. readonly DRIVER_ZIP_FILE="$TARGET_DIR/chromedriver.zip" -wget --no-verbose "https://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip" -O "$DRIVER_ZIP_FILE" -unzip "$DRIVER_ZIP_FILE" -d "$TARGET_DIR/chromedriver" +wget --no-verbose "${DOWNLOAD_ROOT}chromedriver_linux64.zip?alt=media" -O "$DRIVER_ZIP_FILE" +unzip -q "$DRIVER_ZIP_FILE" -d "$TARGET_DIR/" +# Rename TARGET_DIR/chromedriver_linux64 to the expected TARGET_DIR/chromedriver +mv -T "$TARGET_DIR/chromedriver_linux64" "$TARGET_DIR/chromedriver" + +export CHROME_EXECUTABLE="$TARGET_DIR/chrome-linux/chrome" # Echo info at the end for ease of debugging. -export CHROME_EXECUTABLE="$TARGET_DIR"/chrome-linux/chrome -echo $CHROME_EXECUTABLE -$CHROME_EXECUTABLE --version -echo "ChromeDriver $CHROME_DRIVER_VERSION" +set +x +echo +readonly CHROMEDRIVER_EXECUTABLE="$TARGET_DIR/chromedriver/chromedriver" +echo "$CHROME_EXECUTABLE" +"$CHROME_EXECUTABLE" --version +echo "$CHROMEDRIVER_EXECUTABLE" +"$CHROMEDRIVER_EXECUTABLE" --version +echo diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 8f2807f0dd03..c492dc00905e 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,144 @@ +## 0.12.1 + +* Modifies `publish_check_command.dart` to do a `dart pub get` in all examples + of the package being checked. Workaround for [dart-lang/pub#3618](https://github.com/dart-lang/pub/issues/3618). + +## 0.12.0 + +* Changes the behavior of `--packages-for-branch` on main/master to run for + packages changed in the last commit, rather than running for all packages. + This allows CI to test the same filtered set of packages in post-submit as are + tested in presubmit. +* Adds a `fix` command to run `dart fix --apply` in target packages. + +## 0.11.0 + +* Renames `publish-plugin` to `publish`. +* Renames arguments to `list`: + * `--package` now lists top-level packages (previously `--plugin`). + * `--package-or-subpackage` now lists top-level packages (previously + `--package`). + +## 0.10.0+1 + +* Recognizes `run_test.sh` as a developer-only file in `version-check`. +* Adds `readme-check` validation that the example/README.md for a federated + plugin's implementation packages has a warning about the intended use of the + example instead of the template boilerplate. + +## 0.10.0 + +* Improves the logic in `version-check` to determine what changes don't require + version changes, as well as making any dev-only changes also not require + changelog changes since in practice we almost always override the check in + that case. +* Removes special-case handling of Dependabot PRs, and the (fragile) + `--change-description-file` flag was only still used for that case, as + the improved diff analysis now handles that case more robustly. + +## 0.9.3 + +* Raises minimum `compileSdkVersion` to 32 for the `all-plugins-app` command. + +## 0.9.2 + +* Adds checking of `code-excerpt` configuration to `readme-check`, to validate + that if the excerpting tags are added to a README they are actually being + used. + +## 0.9.1 + +* Adds a `--downgrade` flag to `analyze` for analyzing with the oldest possible + versions of packages. + +## 0.9.0 + +* Replaces PR-description-based version/changelog/breaking change check + overrides in `version-check` with label-based overrides using a new + `pr-labels` flag, since we don't actually have reliable access to the + PR description in checks. + +## 0.8.10 + +- Adds a new `remove-dev-dependencies` command to remove `dev_dependencies` + entries to make legacy version analysis possible in more cases. +- Adds a `--lib-only` option to `analyze` to allow only analyzing the client + parts of a library for legacy verison compatibility. + +## 0.8.9 + +- Includes `dev_dependencies` when overridding dependencies using + `make-deps-path-based`. +- Bypasses version and CHANGELOG checks for Dependabot PRs for packages + that are known not to be client-affecting. + +## 0.8.8 + +- Allows pre-release versions in `version-check`. + +## 0.8.7 + +- Supports empty custom analysis allow list files. +- `drive-examples` now validates files to ensure that they don't accidentally + use `test(...)`. +- Adds a new `dependabot-check` command to ensure complete Dependabot coverage. +- Adds `skip-if-not-supporting-dart-version` to allow for the same use cases + as `skip-if-not-supporting-flutter-version` but for packages without Flutter + constraints. + +## 0.8.6 + +- Adds `update-release-info` to apply changelog and optional version changes + across multiple packages. +- Fixes changelog validation when reverting to a `NEXT` state. +- Fixes multiplication of `--force` flag when publishing multiple packages. +- Adds minimum deployment target flags to `xcode-analyze` to allow + enforcing deprecation warning handling in advance of actually dropping + support for an OS version. +- Checks for template boilerplate in `readme-check`. +- `readme-check` now validates example READMEs when present. + +## 0.8.5 + +- Updates `test` to inculde the Dart unit tests of examples, if any. +- `drive-examples` now supports non-plugin packages. +- Commands that iterate over examples now include non-Flutter example packages. + +## 0.8.4 + +- `readme-check` now validates that there's a info tag on code blocks to + identify (and for supported languages, syntax highlight) the language. +- `readme-check` now has a `--require-excerpts` flag to require that any Dart + code blocks be managed by `code_excerpter`. + +## 0.8.3 + +- Adds a new `update-excerpts` command to maintain README files using the + `code-excerpter` package from flutter/site-shared. +- `license-check` now ignores submodules. +- Allows `make-deps-path-based` to skip packages it has alredy rewritten, so + that running multiple times won't fail after the first time. +- Removes UWP support, since Flutter has dropped support for UWP. + +## 0.8.2+1 + +- Adds a new `readme-check` command. +- Updates `publish-plugin` command documentation. +- Fixes `all-plugins-app` to preserve the original application's Dart SDK + version to avoid changing language feature opt-ins that the template may + rely on. +- Fixes `custom-test` to run `pub get` before running Dart test scripts. + +## 0.8.2 + +- Adds a new `custom-test` command. +- Switches from deprecated `flutter packages` alias to `flutter pub`. + +## 0.8.1 + +- Fixes an `analyze` regression in 0.8.0 with packages that have non-`example` + sub-packages. + ## 0.8.0 - Ensures that `firebase-test-lab` runs include an `integration_test` runner. @@ -19,6 +160,9 @@ `flutter` behavior. - Validates `default_package` entries in plugins. - Removes `allow-warnings` from the `podspecs` command. +- Adds `skip-if-not-supporting-flutter-version` to allow running tests using a + version of Flutter that not all packages support. (E.g., to allow for running + some tests against old versions of Flutter to help avoid accidental breakage.) ## 0.7.3 diff --git a/script/tool/README.md b/script/tool/README.md index 265d3868fc37..9f0ac84145f2 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -15,6 +15,14 @@ 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: @@ -46,7 +54,7 @@ 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 plugin. +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. @@ -63,29 +71,29 @@ command is targetting. An package name can be any of: ```sh cd -dart run ./script/tool/bin/flutter_plugin_tools.dart format --packages plugin_name +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 plugin_name +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 plugin_name +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 plugin_name -dart run ./script/tool/bin/flutter_plugin_tools.dart drive-examples --android --packages plugin_name +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 @@ -102,35 +110,76 @@ 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 plugin_name +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 plugin_name +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 -``sh +**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-plugin --package -`` +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-plugin --help` for more usage information. +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-plugin` will first check the status of the local +`publish` will first check the status of the local directory and refuse to publish if there are any mismatched files with version control present. -Automated publishing is under development. Follow -[flutter/flutter#27258](https://github.com/flutter/flutter/issues/27258) -for updates. - ## Updating the Tool For flutter/plugins, just changing the source here is all that's needed. @@ -139,4 +188,4 @@ 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/master/.cirrus.yml) + [flutter/packages](https://github.com/flutter/packages/blob/main/.cirrus.yml) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index faad7f4736eb..c7a953c50cac 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -10,12 +10,9 @@ import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; -import 'common/plugin_command.dart'; import 'common/process_runner.dart'; import 'common/repository_package.dart'; -const int _exitPackagesGetFailed = 3; - /// A command to run Dart analysis on packages. class AnalyzeCommand extends PackageLoopingCommand { /// Creates a analysis command instance. @@ -35,10 +32,17 @@ 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; @@ -84,48 +88,17 @@ class AnalyzeCommand extends PackageLoopingCommand { return false; } - /// Ensures that the dependent packages have been fetched for all packages - /// (including their sub-packages) that will be analyzed. - Future _runPackagesGetOnTargetPackages() async { - final List packageDirectories = - await getTargetPackagesAndSubpackages() - .map((PackageEnumerationEntry entry) => entry.package.directory) - .toList(); - final Set packagePaths = - packageDirectories.map((Directory dir) => dir.path).toSet(); - packageDirectories.removeWhere((Directory directory) { - // Remove the 'example' subdirectories; 'flutter packages get' - // automatically runs 'pub get' there as part of handling the parent - // directory. - return directory.basename == 'example' && - packagePaths.contains(directory.parent.path); - }); - for (final Directory package in packageDirectories) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, ['packages', 'get'], - workingDir: package); - if (exitCode != 0) { - return false; - } - } - return true; - } - @override Future initializeRun() async { - print('Fetching dependencies...'); - if (!await _runPackagesGetOnTargetPackages()) { - printError('Unable to get dependencies.'); - throw ToolExit(_exitPackagesGetFailed); - } - _allowedCustomAnalysisDirectories = getStringListArg(_customAnalysisFlag).expand((String item) { if (item.endsWith('.yaml')) { final File file = packagesDir.fileSystem.file(item); - return (loadYaml(file.readAsStringSync()) as YamlList) - .toList() - .cast(); + final Object? yaml = loadYaml(file.readAsStringSync()); + if (yaml == null) { + return []; + } + return (yaml as YamlList).toList().cast(); } return [item]; }).toSet(); @@ -138,15 +111,54 @@ 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' + // automatically runs `pub get` in examples as part of handling the parent + // directory. + final List packagesToGet = [ + package, + if (!libOnly) ...await getSubpackages(package).toList(), + ]; + for (final RepositoryPackage packageToGet in packagesToGet) { + if (packageToGet.directory.basename != 'example' || + !RepositoryPackage(packageToGet.directory.parent) + .pubspecFile + .existsSync()) { + if (!await _runPubCommand(packageToGet, 'get')) { + return PackageResult.fail(['Unable to get dependencies']); + } + } + } + if (_hasUnexpecetdAnalysisOptions(package)) { return PackageResult.fail(['Unexpected local analysis options']); } - final int exitCode = await processRunner.runAndStream( - _dartBinaryPath, ['analyze', '--fatal-infos'], + final int exitCode = await processRunner.runAndStream(_dartBinaryPath, + ['analyze', '--fatal-infos', if (libOnly) 'lib'], workingDir: package.directory); if (exitCode != 0) { return PackageResult.fail(); } return PackageResult.success(); } + + Future _runPubCommand(RepositoryPackage package, String command) async { + final int exitCode = await processRunner.runAndStream( + flutterCommand, ['pub', command], + workingDir: package.directory); + return exitCode == 0; + } } diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index b88cfe309258..1aade3575559 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -37,8 +37,7 @@ const String _flutterBuildTypeIOS = 'ios'; const String _flutterBuildTypeLinux = 'linux'; const String _flutterBuildTypeMacOS = 'macos'; const String _flutterBuildTypeWeb = 'web'; -const String _flutterBuildTypeWin32 = 'windows'; -const String _flutterBuildTypeWinUwp = 'winuwp'; +const String _flutterBuildTypeWindows = 'windows'; /// A command to build the example applications for packages. class BuildExamplesCommand extends PackageLoopingCommand { @@ -52,7 +51,6 @@ class BuildExamplesCommand extends PackageLoopingCommand { argParser.addFlag(platformMacOS); argParser.addFlag(platformWeb); argParser.addFlag(platformWindows); - argParser.addFlag(platformWinUwp); argParser.addFlag(platformIOS); argParser.addFlag(_platformFlagApk); argParser.addOption( @@ -93,16 +91,9 @@ class BuildExamplesCommand extends PackageLoopingCommand { flutterBuildType: _flutterBuildTypeWeb, ), platformWindows: const _PlatformDetails( - 'Win32', + 'Windows', pluginPlatform: platformWindows, - pluginPlatformVariant: platformVariantWin32, - flutterBuildType: _flutterBuildTypeWin32, - ), - platformWinUwp: const _PlatformDetails( - 'UWP', - pluginPlatform: platformWindows, - pluginPlatformVariant: platformVariantWinUwp, - flutterBuildType: _flutterBuildTypeWinUwp, + flutterBuildType: _flutterBuildTypeWindows, ), }; @@ -146,9 +137,8 @@ class BuildExamplesCommand extends PackageLoopingCommand { // no package-level platform information for non-plugin packages. final Set<_PlatformDetails> buildPlatforms = isPlugin ? requestedPlatforms - .where((_PlatformDetails platform) => pluginSupportsPlatform( - platform.pluginPlatform, package, - variant: platform.pluginPlatformVariant)) + .where((_PlatformDetails platform) => + pluginSupportsPlatform(platform.pluginPlatform, package)) .toSet() : requestedPlatforms.toSet(); @@ -280,22 +270,6 @@ class BuildExamplesCommand extends PackageLoopingCommand { }) async { final String enableExperiment = getStringArg(kEnableExperiment); - // The UWP template is not yet stable, so the UWP directory - // needs to be created on the fly with 'flutter create .' - Directory? temporaryPlatformDirectory; - if (flutterBuildType == _flutterBuildTypeWinUwp) { - final Directory uwpDirectory = example.directory.childDirectory('winuwp'); - if (!uwpDirectory.existsSync()) { - print('Creating temporary winuwp folder'); - final int exitCode = await processRunner.runAndStream(flutterCommand, - ['create', '--platforms=$platformWinUwp', '.'], - workingDir: example.directory); - if (exitCode == 0) { - temporaryPlatformDirectory = uwpDirectory; - } - } - } - final int exitCode = await processRunner.runAndStream( flutterCommand, [ @@ -308,13 +282,6 @@ class BuildExamplesCommand extends PackageLoopingCommand { ], workingDir: example.directory, ); - - if (temporaryPlatformDirectory != null && - temporaryPlatformDirectory.existsSync()) { - print('Cleaning up ${temporaryPlatformDirectory.path}'); - temporaryPlatformDirectory.deleteSync(recursive: true); - } - return exitCode == 0; } } @@ -324,7 +291,6 @@ class _PlatformDetails { const _PlatformDetails( this.label, { required this.pluginPlatform, - this.pluginPlatformVariant, required this.flutterBuildType, this.extraBuildFlags = const [], }); @@ -335,10 +301,6 @@ class _PlatformDetails { /// The key in a pubspec's platform: entry. final String pluginPlatform; - /// The supportedVariants key under a plugin's [pluginPlatform] entry, if - /// applicable. - final String? pluginPlatformVariant; - /// The `flutter build` build type. final String flutterBuildType; diff --git a/script/tool/lib/src/common/cmake.dart b/script/tool/lib/src/common/cmake.dart index 04ad880292b9..3f5d8452bd44 100644 --- a/script/tool/lib/src/common/cmake.dart +++ b/script/tool/lib/src/common/cmake.dart @@ -3,9 +3,9 @@ // found in the LICENSE file. import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:platform/platform.dart'; +import 'core.dart'; import 'process_runner.dart'; const String _cacheCommandKey = 'CMAKE_COMMAND:INTERNAL'; diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index 15a0d6f1f3b2..b91029f1a5c8 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -4,7 +4,6 @@ import 'package:colorize/colorize.dart'; import 'package:file/file.dart'; -import 'package:yaml/yaml.dart'; /// The signature for a print handler for commands that allow overriding the /// print destination. @@ -26,50 +25,27 @@ const String platformMacOS = 'macos'; const String platformWeb = 'web'; /// Key for windows platform. -/// -/// Note that this corresponds to the Win32 variant for flutter commands like -/// `build` and `run`, but is a general platform containing all Windows -/// variants for purposes of the `platform` section of a plugin pubspec). const String platformWindows = 'windows'; -/// Key for WinUWP platform. -/// -/// Note that UWP is a platform for the purposes of flutter commands like -/// `build` and `run`, but a variant of the `windows` platform for the purposes -/// of plugin pubspecs). -const String platformWinUwp = 'winuwp'; - -/// Key for Win32 variant of the Windows platform. -const String platformVariantWin32 = 'win32'; - -/// Key for UWP variant of the Windows platform. -/// -/// See the note on [platformWinUwp]. -const String platformVariantWinUwp = 'uwp'; - /// Key for enable experiment. const String kEnableExperiment = 'enable-experiment'; -/// Returns whether the given directory contains a Flutter package. -bool isFlutterPackage(FileSystemEntity entity) { - if (entity is! Directory) { - return false; - } +/// Target platforms supported by Flutter. +// ignore: public_member_api_docs +enum FlutterPlatform { android, ios, linux, macos, web, windows } - try { - final File pubspecFile = entity.childFile('pubspec.yaml'); - final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap? dependencies = pubspecYaml['dependencies'] as YamlMap?; - if (dependencies == null) { - return false; - } - return dependencies.containsKey('flutter'); - } on FileSystemException { - return false; - } on YamlException { +/// Returns whether the given directory is a Dart package. +bool isPackage(FileSystemEntity entity) { + if (entity is! Directory) { return false; } + // According to + // https://dart.dev/guides/libraries/create-library-packages#what-makes-a-library-package + // a package must also have a `lib/` directory, but in practice that's not + // always true. flutter/plugins has some special cases (espresso, some + // federated implementation packages) that don't have any source, so this + // deliberately doesn't check that there's a lib directory. + return entity.childFile('pubspec.yaml').existsSync(); } /// Prints `successMessage` in green. diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart index 32d30e60feb5..b135424827a6 100644 --- a/script/tool/lib/src/common/git_version_finder.dart +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -50,6 +50,27 @@ class GitVersionFinder { return changedFiles.toList(); } + /// Get a list of all the changed files. + Future> getDiffContents({ + String? targetPath, + bool includeUncommitted = false, + }) async { + final String baseSha = await getBaseSha(); + final io.ProcessResult diffCommand = await baseGitDir.runCommand([ + 'diff', + baseSha, + if (!includeUncommitted) 'HEAD', + if (targetPath != null) ...['--', targetPath], + ]); + final String diffStdout = diffCommand.stdout.toString(); + if (diffStdout.isEmpty) { + return []; + } + final List changedFiles = diffStdout.split('\n') + ..removeWhere((String element) => element.isEmpty); + return changedFiles.toList(); + } + /// Get the package version specified in the pubspec file in `pubspecPath` and /// at the revision of `gitRef` (defaulting to the base if not provided). Future getPackageVersion(String pubspecPath, @@ -82,7 +103,7 @@ class GitVersionFinder { ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], throwOnError: false); final String stdout = (baseShaFromMergeBase.stdout as String? ?? '').trim(); - final String stderr = (baseShaFromMergeBase.stdout as String? ?? '').trim(); + final String stderr = (baseShaFromMergeBase.stderr as String? ?? '').trim(); if (stderr.isNotEmpty || stdout.isEmpty) { baseShaFromMergeBase = await baseGitDir .runCommand(['merge-base', 'FETCH_HEAD', 'HEAD']); diff --git a/script/tool/lib/src/common/gradle.dart b/script/tool/lib/src/common/gradle.dart index 9da4e89811e6..746536075014 100644 --- a/script/tool/lib/src/common/gradle.dart +++ b/script/tool/lib/src/common/gradle.dart @@ -6,6 +6,7 @@ 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'; @@ -21,7 +22,7 @@ class GradleProject { }); /// The directory of a Flutter project to run Gradle commands in. - final Directory flutterProject; + final RepositoryPackage flutterProject; /// The [ProcessRunner] used to run commands. Overridable for testing. final ProcessRunner processRunner; @@ -30,7 +31,8 @@ class GradleProject { final Platform platform; /// The project's 'android' directory. - Directory get androidDirectory => flutterProject.childDirectory('android'); + Directory get androidDirectory => + flutterProject.platformDirectory(FlutterPlatform.android); /// The path to the Gradle wrapper file for the project. File get gradleWrapper => androidDirectory.childFile( diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/package_command.dart similarity index 83% rename from script/tool/lib/src/common/plugin_command.dart rename to script/tool/lib/src/common/package_command.dart index 184663568224..0e83d03e9846 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/package_command.dart @@ -34,9 +34,9 @@ class PackageEnumerationEntry { /// Interface definition for all commands in this tool. // TODO(stuartmorgan): Move most of this logic to PackageLoopingCommand. -abstract class PluginCommand extends Command { +abstract class PackageCommand extends Command { /// Creates a command to operate on [packagesDir] with the given environment. - PluginCommand( + PackageCommand( this.packagesDir, { this.processRunner = const ProcessRunner(), this.platform = const LocalPlatform(), @@ -44,11 +44,10 @@ abstract class PluginCommand extends Command { }) : _gitDir = gitDir { argParser.addMultiOption( _packagesArg, - splitCommas: true, help: 'Specifies which packages the command should run on (before sharding).\n', valueHelp: 'package1,package2,...', - aliases: [_pluginsArg], + aliases: [_pluginsLegacyAliasArg], ); argParser.addOption( _shardIndexArg, @@ -59,7 +58,7 @@ abstract class PluginCommand extends Command { ); argParser.addOption( _shardCountArg, - help: 'Specifies the number of shards into which plugins are divided.', + help: 'Specifies the number of shards into which packages are divided.', valueHelp: 'n', defaultsTo: '1', ); @@ -72,7 +71,7 @@ abstract class PluginCommand extends Command { defaultsTo: [], ); argParser.addFlag(_runOnChangedPackagesArg, - help: 'Run the command on changed packages/plugins.\n' + help: 'Run the command on changed packages.\n' 'If no packages have changed, or if there have been changes that may\n' 'affect all packages, the command runs on all packages.\n' 'Packages excluded with $_excludeArg are excluded even if changed.\n' @@ -85,9 +84,8 @@ abstract class PluginCommand extends Command { 'Cannot be combined with $_packagesArg.\n', hide: true); argParser.addFlag(_packagesForBranchArg, - help: - 'This runs on all packages (equivalent to no package selection flag)\n' - 'on main (or master), and behaves like --run-on-changed-packages on ' + help: 'This runs on all packages changed in the last commit on main ' + '(or master), and behaves like --run-on-changed-packages on ' 'any other branch.\n\n' 'Cannot be combined with $_packagesArg.\n\n' 'This is intended for use in CI.\n', @@ -107,13 +105,13 @@ abstract class PluginCommand extends Command { static const String _logTimingArg = 'log-timing'; static const String _packagesArg = 'packages'; static const String _packagesForBranchArg = 'packages-for-branch'; - static const String _pluginsArg = 'plugins'; + static const String _pluginsLegacyAliasArg = 'plugins'; static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; static const String _runOnDirtyPackagesArg = 'run-on-dirty-packages'; static const String _shardCountArg = 'shardCount'; static const String _shardIndexArg = 'shardIndex'; - /// The directory containing the plugin packages. + /// The directory containing the packages. final Directory packagesDir; /// The process runner. @@ -192,7 +190,9 @@ abstract class PluginCommand extends Command { /// Convenience accessor for List arguments. List getStringListArg(String key) { - return (argResults![key] as List?) ?? []; + // Clone the list so that if a caller modifies the result it won't change + // the actual arguments list for future queries. + return List.from(argResults![key] as List? ?? []); } /// If true, commands should log timing information that might be useful in @@ -220,7 +220,7 @@ abstract class PluginCommand extends Command { _shardCount = shardCount; } - /// Returns the set of plugins to exclude based on the `--exclude` argument. + /// Returns the set of packages to exclude based on the `--exclude` argument. Set getExcludedPackageNames() { final Set excludedPackages = _excludedPackages ?? getStringListArg(_excludeArg).expand((String item) { @@ -249,22 +249,22 @@ abstract class PluginCommand extends Command { Stream getTargetPackages( {bool filterExcluded = true}) async* { // To avoid assuming consistency of `Directory.list` across command - // invocations, we collect and sort the plugin folders before sharding. + // invocations, we collect and sort the package folders before sharding. // This is considered an implementation detail which is why the API still // uses streams. - final List allPlugins = + final List allPackages = await _getAllPackages().toList(); - allPlugins.sort((PackageEnumerationEntry p1, PackageEnumerationEntry p2) => + allPackages.sort((PackageEnumerationEntry p1, PackageEnumerationEntry p2) => p1.package.path.compareTo(p2.package.path)); - final int shardSize = allPlugins.length ~/ shardCount + - (allPlugins.length % shardCount == 0 ? 0 : 1); - final int start = min(shardIndex * shardSize, allPlugins.length); - final int end = min(start + shardSize, allPlugins.length); - - for (final PackageEnumerationEntry plugin - in allPlugins.sublist(start, end)) { - if (!(filterExcluded && plugin.excluded)) { - yield plugin; + final int shardSize = allPackages.length ~/ shardCount + + (allPackages.length % shardCount == 0 ? 0 : 1); + final int start = min(shardIndex * shardSize, allPackages.length); + final int end = min(start + shardSize, allPackages.length); + + for (final PackageEnumerationEntry package + in allPackages.sublist(start, end)) { + if (!(filterExcluded && package.excluded)) { + yield package; } } } @@ -310,9 +310,9 @@ abstract class PluginCommand extends Command { Set packages = Set.from(getStringListArg(_packagesArg)); - final bool runOnChangedPackages; + final GitVersionFinder? changedFileFinder; if (getBoolArg(_runOnChangedPackagesArg)) { - runOnChangedPackages = true; + changedFileFinder = await retrieveVersionFinder(); } else if (getBoolArg(_packagesForBranchArg)) { final String? branch = await _getBranch(); if (branch == null) { @@ -320,25 +320,32 @@ abstract class PluginCommand extends Command { 'only be used in a git repository.'); throw ToolExit(exitInvalidArguments); } else { - runOnChangedPackages = branch != 'master' && branch != 'main'; - // Log the mode for auditing what was intended to run. - print('--$_packagesForBranchArg: running on ' - '${runOnChangedPackages ? 'changed' : 'all'} packages'); + // Configure the change finder the correct mode for the branch. + final bool lastCommitOnly = branch == 'main' || branch == 'master'; + 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.'); + changedFileFinder = GitVersionFinder(await gitDir, 'HEAD~'); + } else { + changedFileFinder = await retrieveVersionFinder(); + } } } else { - runOnChangedPackages = false; + changedFileFinder = null; } - final Set excludedPluginNames = getExcludedPackageNames(); - - if (runOnChangedPackages) { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final String baseSha = await gitVersionFinder.getBaseSha(); - print( - 'Running for all packages that have changed relative to "$baseSha"\n'); + if (changedFileFinder != null) { + final String baseSha = await changedFileFinder.getBaseSha(); final List changedFiles = - await gitVersionFinder.getChangedFiles(); - if (!_changesRequireFullTest(changedFiles)) { + await changedFileFinder.getChangedFiles(); + if (_changesRequireFullTest(changedFiles)) { + print('Running for all packages, since a file has changed that could ' + 'affect the entire repository.'); + } else { + print( + 'Running for all packages that have diffs relative to "$baseSha"\n'); packages = _getChangedPackageNames(changedFiles); } } else if (getBoolArg(_runOnDirtyPackagesArg)) { @@ -361,24 +368,26 @@ abstract class PluginCommand extends Command { .childDirectory('third_party') .childDirectory('packages'); + final Set excludedPackageNames = getExcludedPackageNames(); for (final Directory dir in [ packagesDir, if (thirdPartyPackagesDirectory.existsSync()) thirdPartyPackagesDirectory, ]) { await for (final FileSystemEntity entity in dir.list(followLinks: false)) { - // A top-level Dart package is a plugin package. - if (_isDartPackage(entity)) { + // A top-level Dart package is a standard package. + if (isPackage(entity)) { if (packages.isEmpty || packages.contains(p.basename(entity.path))) { yield PackageEnumerationEntry( RepositoryPackage(entity as Directory), - excluded: excludedPluginNames.contains(entity.basename)); + excluded: excludedPackageNames.contains(entity.basename)); } } else if (entity is Directory) { - // Look for Dart packages under this top-level directory. + // Look for Dart packages under this top-level directory; this is the + // standard structure for federated plugins. await for (final FileSystemEntity subdir in entity.list(followLinks: false)) { - if (_isDartPackage(subdir)) { + if (isPackage(subdir)) { // There are three ways for a federated plugin to match: // - package name (path_provider_android) // - fully specified name (path_provider/path_provider_android) @@ -393,7 +402,7 @@ abstract class PluginCommand extends Command { packages.intersection(possibleMatches).isNotEmpty) { yield PackageEnumerationEntry( RepositoryPackage(subdir as Directory), - excluded: excludedPluginNames + excluded: excludedPackageNames .intersection(possibleMatches) .isNotEmpty); } @@ -409,21 +418,31 @@ abstract class PluginCommand extends Command { /// /// By default, packages excluded via --exclude will not be in the stream, but /// they can be included by passing false for [filterExcluded]. + /// + /// Subpackages are guaranteed to be after the containing package in the + /// stream. Stream getTargetPackagesAndSubpackages( {bool filterExcluded = true}) async* { - await for (final PackageEnumerationEntry plugin + await for (final PackageEnumerationEntry package in getTargetPackages(filterExcluded: filterExcluded)) { - yield plugin; - yield* plugin.package.directory - .list(recursive: true, followLinks: false) - .where(_isDartPackage) - .map((FileSystemEntity directory) => PackageEnumerationEntry( - // _isDartPackage guarantees that this cast is valid. - RepositoryPackage(directory as Directory), - excluded: plugin.excluded)); + yield package; + yield* getSubpackages(package.package).map( + (RepositoryPackage subPackage) => + PackageEnumerationEntry(subPackage, excluded: package.excluded)); } } + /// Returns all Dart package folders (e.g., examples) under the given package. + Stream getSubpackages(RepositoryPackage package, + {bool filterExcluded = true}) async* { + yield* package.directory + .list(recursive: true, followLinks: false) + .where(isPackage) + .map((FileSystemEntity directory) => + // isPackage guarantees that this cast is valid. + RepositoryPackage(directory as Directory)); + } + /// Returns the files contained, recursively, within the packages /// involved in this command execution. Stream getFiles() { @@ -439,12 +458,6 @@ abstract class PluginCommand extends Command { .cast(); } - /// Returns whether the specified entity is a directory containing a - /// `pubspec.yaml` file. - bool _isDartPackage(FileSystemEntity entity) { - return entity is Directory && entity.childFile('pubspec.yaml').existsSync(); - } - /// Retrieve an instance of [GitVersionFinder] based on `_baseShaArg` and [gitDir]. /// /// Throws tool exit if [gitDir] nor root directory is a git directory. @@ -520,7 +533,7 @@ abstract class PluginCommand extends Command { } // Returns true if one or more files changed that have the potential to affect - // any plugin (e.g., CI script changes). + // any packages (e.g., CI script changes). bool _changesRequireFullTest(List changedFiles) { const List specialFiles = [ '.ci.yaml', // LUCI config. diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index bfee71a0f4c2..d8b1cf001d13 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -9,12 +9,27 @@ 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 'core.dart'; -import 'plugin_command.dart'; +import 'package_command.dart'; import 'process_runner.dart'; import 'repository_package.dart'; +/// Enumeration options for package looping commands. +enum PackageLoopingType { + /// Only enumerates the top level packages, without including any of their + /// subpackages. + topLevelOnly, + + /// Enumerates the top level packages and any example packages they contain. + includeExamples, + + /// Enumerates all packages recursively, including both example and + /// non-example subpackages. + includeAllSubpackages, +} + /// Possible outcomes of a command run for a package. enum RunState { /// The command succeeded for the package. @@ -67,7 +82,7 @@ class PackageResult { /// An abstract base class for a command that iterates over a set of packages /// controlled by a standard set of flags, running some actions on each package, /// and collecting and reporting the success/failure of those actions. -abstract class PackageLoopingCommand extends PluginCommand { +abstract class PackageLoopingCommand extends PackageCommand { /// Creates a command to operate on [packagesDir] with the given environment. PackageLoopingCommand( Directory packagesDir, { @@ -75,7 +90,23 @@ abstract class PackageLoopingCommand extends PluginCommand { Platform platform = const LocalPlatform(), GitDir? gitDir, }) : super(packagesDir, - processRunner: processRunner, platform: platform, gitDir: gitDir); + processRunner: processRunner, platform: platform, gitDir: gitDir) { + argParser.addOption( + _skipByFlutterVersionArg, + help: 'Skip any packages that require a Flutter version newer than ' + 'the provided version.', + ); + argParser.addOption( + _skipByDartVersionArg, + help: 'Skip any packages that require a Dart version newer than ' + 'the provided version.', + ); + } + + static const String _skipByFlutterVersionArg = + 'skip-if-not-supporting-flutter-version'; + static const String _skipByDartVersionArg = + 'skip-if-not-supporting-dart-version'; /// Packages that had at least one [logWarning] call. final Set _packagesWithWarnings = @@ -99,9 +130,26 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Note: Consistent behavior across commands whenever possibel is a goal for /// this tool, so this should be overridden only in rare cases. Stream getPackagesToProcess() async* { - yield* includeSubpackages - ? getTargetPackagesAndSubpackages(filterExcluded: false) - : getTargetPackages(filterExcluded: false); + switch (packageLoopingType) { + case PackageLoopingType.topLevelOnly: + yield* getTargetPackages(filterExcluded: false); + break; + case PackageLoopingType.includeExamples: + await for (final PackageEnumerationEntry packageEntry + in getTargetPackages(filterExcluded: false)) { + yield packageEntry; + yield* Stream.fromIterable(packageEntry + .package + .getExamples() + .map((RepositoryPackage package) => PackageEnumerationEntry( + package, + excluded: packageEntry.excluded))); + } + break; + case PackageLoopingType.includeAllSubpackages: + yield* getTargetPackagesAndSubpackages(filterExcluded: false); + break; + } } /// Runs the command for [package], returning a list of errors. @@ -130,9 +178,9 @@ abstract class PackageLoopingCommand extends PluginCommand { /// to make the output structure easier to follow. bool get hasLongOutput => true; - /// Whether to loop over all packages (e.g., including example/), rather than - /// only top-level packages. - bool get includeSubpackages => false; + /// Whether to loop over top-level packages only, or some or all of their + /// sub-packages as well. + PackageLoopingType get packageLoopingType => PackageLoopingType.topLevelOnly; /// The text to output at the start when reporting one or more failures. /// This will be followed by a list of packages that reported errors, with @@ -219,6 +267,14 @@ abstract class PackageLoopingCommand extends PluginCommand { _otherWarningCount = 0; _currentPackageEntry = null; + final String minFlutterVersionArg = getStringArg(_skipByFlutterVersionArg); + final Version? minFlutterVersion = minFlutterVersionArg.isEmpty + ? null + : Version.parse(minFlutterVersionArg); + final String minDartVersionArg = getStringArg(_skipByDartVersionArg); + final Version? minDartVersion = + minDartVersionArg.isEmpty ? null : Version.parse(minDartVersionArg); + final DateTime runStart = DateTime.now(); await initializeRun(); @@ -242,7 +298,9 @@ abstract class PackageLoopingCommand extends PluginCommand { PackageResult result; try { - result = await runForPackage(entry.package); + result = await _runForPackageIfSupported(entry.package, + minFlutterVersion: minFlutterVersion, + minDartVersion: minDartVersion); } catch (e, stack) { printError(e.toString()); printError(stack.toString()); @@ -285,6 +343,35 @@ abstract class PackageLoopingCommand extends PluginCommand { return true; } + /// Returns the result of running [runForPackage] if the package is supported + /// by any run constraints, or a skip result if it is not. + Future _runForPackageIfSupported( + RepositoryPackage package, { + Version? minFlutterVersion, + Version? minDartVersion, + }) async { + if (minFlutterVersion != null) { + final Pubspec pubspec = package.parsePubspec(); + final VersionConstraint? flutterConstraint = + pubspec.environment?['flutter']; + if (flutterConstraint != null && + !flutterConstraint.allows(minFlutterVersion)) { + return PackageResult.skip( + 'Does not support Flutter $minFlutterVersion'); + } + } + + if (minDartVersion != null) { + final Pubspec pubspec = package.parsePubspec(); + final VersionConstraint? dartConstraint = pubspec.environment?['sdk']; + if (dartConstraint != null && !dartConstraint.allows(minDartVersion)) { + return PackageResult.skip('Does not support Dart $minDartVersion'); + } + } + + return await runForPackage(package); + } + void _printSuccess(String message) { captureOutput ? print(message) : printSuccess(message); } @@ -341,9 +428,9 @@ abstract class PackageLoopingCommand extends PluginCommand { .length; // Split the warnings into those from packages that ran, and those that // were skipped. - final Set _skippedPackagesWithWarnings = + final Set skippedPackagesWithWarnings = _packagesWithWarnings.intersection(skippedPackages); - final int skippedWarningCount = _skippedPackagesWithWarnings.length; + final int skippedWarningCount = skippedPackagesWithWarnings.length; final int runWarningCount = _packagesWithWarnings.length - skippedWarningCount; diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart new file mode 100644 index 000000000000..65f311974f3a --- /dev/null +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -0,0 +1,222 @@ +// 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('androidTest') || + pathComponents.contains('RunnerTests') || + pathComponents.contains('RunnerUITests'); +} + +// 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 index 081ce7f1e815..94677fe7e5a3 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -2,11 +2,10 @@ // 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:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:yaml/yaml.dart'; import 'core.dart'; +import 'repository_package.dart'; /// Possible plugin support options for a platform. enum PlatformSupport { @@ -37,7 +36,6 @@ bool pluginSupportsPlatform( String platform, RepositoryPackage plugin, { PlatformSupport? requiredMode, - String? variant, }) { assert(platform == platformIOS || platform == platformAndroid || @@ -61,26 +59,6 @@ bool pluginSupportsPlatform( } } - // If a variant is specified, check for that variant. - if (variant != null) { - const String variantsKey = 'supportedVariants'; - if (platformEntry.containsKey(variantsKey)) { - if (!(platformEntry['supportedVariants']! as YamlList) - .contains(variant)) { - return false; - } - } else { - // Platforms with variants have a default variant when unspecified for - // backward compatibility. Must match the flutter tool logic. - const Map defaultVariants = { - platformWindows: platformVariantWin32, - }; - if (variant != defaultVariants[platform]) { - return false; - } - } - } - return true; } @@ -132,13 +110,8 @@ YamlMap? _readPlatformPubspecSectionForPlugin( /// 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 File pubspecFile = package.pubspecFile; - if (!pubspecFile.existsSync()) { - return null; - } - final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; + final Pubspec pubspec = package.parsePubspec(); + final Map? flutterSection = pubspec.flutter; if (flutterSection == null) { return null; } diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index e0c4e4a83bfe..5f448d36d7e2 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -8,6 +8,9 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'core.dart'; +export 'package:pubspec_parse/pubspec_parse.dart' show Pubspec; +export 'core.dart' show FlutterPlatform; + /// A package in the repository. // // TODO(stuartmorgan): Add more package-related info here, such as an on-demand @@ -48,6 +51,47 @@ class RepositoryPackage { /// The package's top-level pubspec.yaml. File get pubspecFile => directory.childFile('pubspec.yaml'); + /// The package's top-level README. + File get readmeFile => directory.childFile('README.md'); + + /// The package's top-level README. + File get changelogFile => directory.childFile('CHANGELOG.md'); + + /// The package's top-level README. + File get authorsFile => directory.childFile('AUTHORS'); + + /// The lib directory containing the package's code. + Directory get libDirectory => directory.childDirectory('lib'); + + /// The test directory containing the package's Dart tests. + Directory get testDirectory => directory.childDirectory('test'); + + /// Returns the directory containing support for [platform]. + Directory platformDirectory(FlutterPlatform platform) { + late final String directoryName; + switch (platform) { + case FlutterPlatform.android: + directoryName = 'android'; + break; + case FlutterPlatform.ios: + directoryName = 'ios'; + break; + case FlutterPlatform.linux: + directoryName = 'linux'; + break; + case FlutterPlatform.macos: + directoryName = 'macos'; + break; + case FlutterPlatform.web: + directoryName = 'web'; + break; + case FlutterPlatform.windows: + directoryName = 'windows'; + break; + } + return directory.childDirectory(directoryName); + } + late final Pubspec _parsedPubspec = Pubspec.parse(pubspecFile.readAsStringSync()); @@ -56,12 +100,24 @@ class RepositoryPackage { /// Caches for future use. Pubspec parsePubspec() => _parsedPubspec; + /// Returns true if the package depends on Flutter. + bool requiresFlutter() { + final Pubspec pubspec = parsePubspec(); + return pubspec.dependencies.containsKey('flutter'); + } + /// True if this appears to be a federated plugin package, according to /// repository conventions. bool get isFederated => directory.parent.basename != 'packages' && directory.basename.startsWith(directory.parent.basename); + /// True if this appears to be the app-facing package of a federated plugin, + /// according to repository conventions. + bool get isAppFacing => + directory.parent.basename != 'packages' && + directory.basename == directory.parent.basename; + /// True if this appears to be a platform interface package, according to /// repository conventions. bool get isPlatformInterface => @@ -82,7 +138,7 @@ class RepositoryPackage { if (!exampleDirectory.existsSync()) { return []; } - if (isFlutterPackage(exampleDirectory)) { + if (isPackage(exampleDirectory)) { return [RepositoryPackage(exampleDirectory)]; } // Only look at the subdirectories of the example directory if the example @@ -90,18 +146,9 @@ class RepositoryPackage { // example directory for other Dart packages. return exampleDirectory .listSync() - .where((FileSystemEntity entity) => isFlutterPackage(entity)) - // isFlutterPackage guarantees that the cast to Directory is safe. + .where((FileSystemEntity entity) => isPackage(entity)) + // isPackage guarantees that the cast to Directory is safe. .map((FileSystemEntity entity) => RepositoryPackage(entity as Directory)); } - - /// Returns the example directory, assuming there is only one. - /// - /// DO NOT USE THIS METHOD. It exists only to easily find code that was - /// written to use a single example and needs to be restructured to handle - /// multiple examples. New code should always use [getExamples]. - // TODO(stuartmorgan): Eliminate all uses of this. - RepositoryPackage getSingleExampleDeprecated() => - RepositoryPackage(directory.childDirectory('example')); } diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart index 82f29bd501f3..ea7d5a5c4388 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -6,22 +6,30 @@ 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/plugin_command.dart'; +import 'common/package_command.dart'; +import 'common/process_runner.dart'; import 'common/repository_package.dart'; const String _outputDirectoryFlag = 'output-dir'; +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 CreateAllPluginsAppCommand extends PluginCommand { +class CreateAllPluginsAppCommand extends PackageCommand { /// Creates an instance of the builder command. CreateAllPluginsAppCommand( Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), Directory? pluginsRoot, - }) : super(packagesDir) { + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { final Directory defaultDir = pluginsRoot ?? packagesDir.fileSystem.currentDirectory; argParser.addOption(_outputDirectoryFlag, @@ -30,11 +38,14 @@ class CreateAllPluginsAppCommand extends PluginCommand { 'Defaults to the repository root.'); } - /// The location of the synthesized app project. - Directory get appDirectory => packagesDir.fileSystem + /// The location to create the synthesized app project. + Directory get _appDirectory => packagesDir.fileSystem .directory(getStringArg(_outputDirectoryFlag)) .childDirectory('all_plugins'); + /// The synthesized app project. + RepositoryPackage get app => RepositoryPackage(_appDirectory); + @override String get description => 'Generate Flutter app that includes all plugins in packages.'; @@ -58,10 +69,28 @@ class CreateAllPluginsAppCommand extends PluginCommand { 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(>[ - _genPubspecWithAllPlugins(), _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(), ]); } @@ -73,7 +102,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { '--template=app', '--project-name=all_plugins', '--android-language=java', - appDirectory.path, + _appDirectory.path, ], ); @@ -83,8 +112,8 @@ class CreateAllPluginsAppCommand extends PluginCommand { } Future _updateAppGradle() async { - final File gradleFile = appDirectory - .childDirectory('android') + final File gradleFile = app + .platformDirectory(FlutterPlatform.android) .childDirectory('app') .childFile('build.gradle'); if (!gradleFile.existsSync()) { @@ -98,8 +127,8 @@ class CreateAllPluginsAppCommand extends PluginCommand { // minSdkVersion 19 is required by WebView. newGradle.writeln('minSdkVersion 20'); } else if (line.contains('compileSdkVersion')) { - // compileSdkVersion 31 is required by Camera. - newGradle.writeln('compileSdkVersion 31'); + // compileSdkVersion 32 is required by webview_flutter. + newGradle.writeln('compileSdkVersion 32'); } else { newGradle.writeln(line); } @@ -107,7 +136,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { newGradle.writeln(' multiDexEnabled true'); } else if (line.contains('dependencies {')) { newGradle.writeln( - ' implementation \'com.google.guava:guava:27.0.1-android\'\n', + " implementation 'com.google.guava:guava:27.0.1-android'\n", ); // Tests for https://github.com/flutter/flutter/issues/43383 newGradle.writeln( @@ -119,8 +148,8 @@ class CreateAllPluginsAppCommand extends PluginCommand { } Future _updateManifest() async { - final File manifestFile = appDirectory - .childDirectory('android') + final File manifestFile = app + .platformDirectory(FlutterPlatform.android) .childDirectory('app') .childDirectory('src') .childDirectory('main') @@ -147,6 +176,18 @@ class CreateAllPluginsAppCommand extends PluginCommand { } 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( @@ -154,9 +195,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { description: 'Flutter app containing all 1st party plugins.', version: Version.parse('1.0.0+1'), environment: { - 'sdk': VersionConstraint.compatibleWith( - Version.parse('2.12.0'), - ), + dartSdkKey: dartSdkConstraint, }, dependencies: { 'flutter': SdkDependency('flutter'), @@ -166,8 +205,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { }, dependencyOverrides: pluginDeps, ); - final File pubspecFile = appDirectory.childFile('pubspec.yaml'); - pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); + app.pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); } Future> _getValidPathDependencies() async { @@ -212,7 +250,12 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} for (final MapEntry entry in values.entries) { buffer.writeln(); if (entry.value is VersionConstraint) { - buffer.write(' ${entry.key}: ${entry.value}'); + String value = entry.value.toString(); + // Range constraints require quoting. + if (value.startsWith('>') || value.startsWith('<')) { + value = "'$value'"; + } + buffer.write(' ${entry.key}: $value'); } else if (entry.value is SdkDependency) { final SdkDependency dep = entry.value as SdkDependency; buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); @@ -242,4 +285,61 @@ dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} 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 new file mode 100644 index 000000000000..0ef6e602c070 --- /dev/null +++ b/script/tool/lib/src/custom_test_command.dart @@ -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. + +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 new file mode 100644 index 000000000000..5aa762e916e5 --- /dev/null +++ b/script/tool/lib/src/dependabot_check_command.dart @@ -0,0 +1,114 @@ +// 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 + .where((dynamic entry) => entry[typeKey] == 'gradle') + .map((dynamic entry) => (entry as YamlMap)[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 index d81153a0fefa..e8fb11b5f289 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -36,10 +36,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { argParser.addFlag(platformWeb, help: 'Runs the web implementation of the examples'); argParser.addFlag(platformWindows, - help: 'Runs the Windows (Win32) implementation of the examples'); - argParser.addFlag(platformWinUwp, - help: - 'Runs the UWP implementation of the examples [currently a no-op]'); + help: 'Runs the Windows implementation of the examples'); argParser.addOption( kEnableExperiment, defaultsTo: '', @@ -52,7 +49,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { final String name = 'drive-examples'; @override - final String description = 'Runs driver tests for plugin example apps.\n\n' + 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 ' @@ -70,7 +67,6 @@ class DriveExamplesCommand extends PackageLoopingCommand { platformMacOS, platformWeb, platformWindows, - platformWinUwp, ]; final int platformCount = platformSwitches .where((String platform) => getBoolArg(platform)) @@ -85,10 +81,6 @@ class DriveExamplesCommand extends PackageLoopingCommand { throw ToolExit(_exitNoPlatformFlags); } - if (getBoolArg(platformWinUwp)) { - logWarning('Driving UWP applications is not yet supported'); - } - String? androidDevice; if (getBoolArg(platformAndroid)) { final List devices = await _getDevicesForPlatform('android'); @@ -126,48 +118,37 @@ class DriveExamplesCommand extends PackageLoopingCommand { ], if (getBoolArg(platformWindows)) platformWindows: ['-d', 'windows'], - // TODO(stuartmorgan): Check these flags once drive supports UWP: - // https://github.com/flutter/flutter/issues/82821 - if (getBoolArg(platformWinUwp)) platformWinUwp: ['-d', 'winuwp'], }; } @override Future runForPackage(RepositoryPackage package) async { - if (package.isPlatformInterface && - !package.getSingleExampleDeprecated().directory.existsSync()) { + 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.'); } - final List deviceFlags = []; - for (final MapEntry> entry - in _targetDeviceFlags.entries) { - final String platform = entry.key; - String? variant; - if (platform == platformWindows) { - variant = platformVariantWin32; - } else if (platform == platformWinUwp) { - variant = platformVariantWinUwp; - // TODO(stuartmorgan): Remove this once drive supports UWP. - // https://github.com/flutter/flutter/issues/82821 - return PackageResult.skip('Drive does not yet support UWP'); + // 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 (pluginSupportsPlatform(platform, package, variant: variant)) { - deviceFlags.addAll(entry.value); - } else { - print('Skipping unsupported platform ${entry.key}...'); + if (unsupportedPlatforms.length == requestedPlatforms.length) { + return PackageResult.skip( + '${package.displayName} does not support any requested platform.'); } } - // If there is no supported target platform, skip the plugin. - if (deviceFlags.isEmpty) { - 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()) { @@ -175,6 +156,15 @@ class DriveExamplesCommand extends PackageLoopingCommand { 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'); @@ -192,7 +182,16 @@ class DriveExamplesCommand extends PackageLoopingCommand { if (legacyTestFile != null) { testTargets.add(legacyTestFile); } else { - (await _getIntegrationTests(example)).forEach(testTargets.add); + 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) { @@ -215,14 +214,41 @@ class DriveExamplesCommand extends PackageLoopingCommand { } } if (!testsRan) { - printError('No driver tests were run ($examplesFound example(s) found).'); - errors.add('No tests ran (use --exclude if this is intentional).'); + // 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 = []; @@ -293,6 +319,25 @@ class DriveExamplesCommand extends PackageLoopingCommand { 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. diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart index df9d86892e13..93a832eb0e29 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -3,17 +3,16 @@ // found in the LICENSE file. import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.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 'package:pubspec_parse/pubspec_parse.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'; diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index e824d8ad1a90..a11284411908 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -59,7 +59,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { help: 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); argParser.addOption('results-bucket', - defaultsTo: 'gs://flutter_firebase_testlab'); + defaultsTo: 'gs://flutter_cirrus_testlab'); argParser.addOption( kEnableExperiment, defaultsTo: '', @@ -119,9 +119,34 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - final RepositoryPackage example = package.getSingleExampleDeprecated(); + 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.directory.childDirectory('android'); + example.platformDirectory(FlutterPlatform.android); if (!androidDirectory.existsSync()) { return PackageResult.skip( '${example.displayName} does not support Android.'); @@ -146,7 +171,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } // Ensures that gradle wrapper exists - final GradleProject project = GradleProject(example.directory, + final GradleProject project = GradleProject(example, processRunner: processRunner, platform: platform); if (!await _ensureGradleWrapperExists(project)) { return PackageResult.fail(['Unable to build example apk']); @@ -163,7 +188,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { // 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(package)) { + for (final File test in _findIntegrationTestFiles(example)) { final String testName = getRelativePosixPath(test, from: package.directory); print('Testing $testName...'); @@ -175,7 +200,8 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { final String buildId = getStringArg('build-id'); final String testRunId = getStringArg('test-run-id'); final String resultsDir = - 'plugins_android_test/${package.displayName}/$buildId/$testRunId/${resultsCounter++}/'; + '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 @@ -299,19 +325,17 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { return true; } - /// Finds and returns all integration test files for [package]. - Iterable _findIntegrationTestFiles(RepositoryPackage package) sync* { - final Directory integrationTestDir = package - .getSingleExampleDeprecated() - .directory - .childDirectory('integration_test'); + /// 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, followLinks: true) + .listSync(recursive: true) .where((FileSystemEntity file) => file is File && file.basename.endsWith('_test.dart')) .cast(); diff --git a/script/tool/lib/src/fix_command.dart b/script/tool/lib/src/fix_command.dart new file mode 100644 index 000000000000..2819609eabbd --- /dev/null +++ b/script/tool/lib/src/fix_command.dart @@ -0,0 +1,51 @@ +// 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 index 10c0779de927..cc6936c566e1 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -11,7 +11,7 @@ import 'package:meta/meta.dart'; import 'package:platform/platform.dart'; import 'common/core.dart'; -import 'common/plugin_command.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 @@ -37,7 +37,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 PluginCommand { +class FormatCommand extends PackageCommand { /// Creates an instance of the format command. FormatCommand( Directory packagesDir, { @@ -130,8 +130,7 @@ class FormatCommand extends PluginCommand { if (clangFiles.isNotEmpty) { final String clangFormat = getStringArg('clang-format'); if (!await _hasDependency(clangFormat)) { - printError( - 'Unable to run \'clang-format\'. Make sure that it is in your ' + 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); } @@ -156,7 +155,7 @@ class FormatCommand extends PluginCommand { final String java = getStringArg('java'); if (!await _hasDependency(java)) { printError( - 'Unable to run \'java\'. Make sure that it is in your path, or ' + 'Unable to run "java". Make sure that it is in your path, or ' 'provide a full path with --java.'); throw ToolExit(_exitDependencyMissing); } diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index d2c129ff7b48..0517bcf43298 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -3,10 +3,12 @@ // 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/plugin_command.dart'; +import 'common/package_command.dart'; const Set _codeFileExtensions = { '.c', @@ -103,9 +105,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. '''; /// Validates that code files have copyright and license blocks. -class LicenseCheckCommand extends PluginCommand { +class LicenseCheckCommand extends PackageCommand { /// Creates a new license check command for [packagesDir]. - LicenseCheckCommand(Directory packagesDir) : super(packagesDir); + LicenseCheckCommand(Directory packagesDir, + {Platform platform = const LocalPlatform(), GitDir? gitDir}) + : super(packagesDir, platform: platform, gitDir: gitDir); @override final String name = 'license-check'; @@ -116,7 +120,14 @@ class LicenseCheckCommand extends PluginCommand { @override Future run() async { - final Iterable allFiles = await _getAllFiles(); + // 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)) && @@ -230,8 +241,7 @@ class LicenseCheckCommand extends PluginCommand { } // Sort by path for more usable output. - final int Function(File, File) pathCompare = - (File a, File b) => a.path.compareTo(b.path); + int pathCompare(File a, File b) => a.path.compareTo(b.path); incorrectFirstPartyFiles.sort(pathCompare); unrecognizedThirdPartyFiles.sort(pathCompare); @@ -275,6 +285,24 @@ class LicenseCheckCommand extends PluginCommand { .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 index 8368160c4c95..eb78ce891685 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -3,12 +3,12 @@ // found in the LICENSE file. import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.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'; @@ -28,35 +28,40 @@ class LintAndroidCommand extends PackageLoopingCommand { @override final String description = 'Runs "gradlew lint" on Android plugins.\n\n' - 'Requires the example to have been build at least once before running.'; + '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 implemenatation.'); + 'Plugin does not have an Android implementation.'); } - final RepositoryPackage example = package.getSingleExampleDeprecated(); - final GradleProject project = GradleProject(example.directory, - processRunner: processRunner, platform: platform); + 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 example before linting']); - } + if (!project.isConfigured()) { + return PackageResult.fail(['Build examples before linting']); + } - final String packageName = package.directory.basename; + 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'); + // 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 exitCode == 0 ? PackageResult.success() : PackageResult.fail(); + return failed ? PackageResult.fail() : PackageResult.success(); } } diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index e45c09bfd2ef..b47657e47eff 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -5,11 +5,11 @@ import 'package:file/file.dart'; import 'package:platform/platform.dart'; -import 'common/plugin_command.dart'; +import 'common/package_command.dart'; import 'common/repository_package.dart'; /// A command to list different types of repository content. -class ListCommand extends PluginCommand { +class ListCommand extends PackageCommand { /// Creates an instance of the list command, whose behavior depends on the /// 'type' argument it provides. ListCommand( @@ -18,14 +18,14 @@ class ListCommand extends PluginCommand { }) : super(packagesDir, platform: platform) { argParser.addOption( _type, - defaultsTo: _plugin, - allowed: [_plugin, _example, _package, _file], + defaultsTo: _package, + allowed: [_package, _example, _allPackage, _file], help: 'What type of file system content to list.', ); } static const String _type = 'type'; - static const String _plugin = 'plugin'; + static const String _allPackage = 'package-or-subpackage'; static const String _example = 'example'; static const String _package = 'package'; static const String _file = 'file'; @@ -39,7 +39,7 @@ class ListCommand extends PluginCommand { @override Future run() async { switch (getStringArg(_type)) { - case _plugin: + case _package: await for (final PackageEnumerationEntry entry in getTargetPackages()) { print(entry.package.path); } @@ -52,7 +52,7 @@ class ListCommand extends PluginCommand { print(package.path); } break; - case _package: + case _allPackage: await for (final PackageEnumerationEntry entry in getTargetPackagesAndSubpackages()) { print(entry.package.path); diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 3e8f19b044dd..414ca7f303c0 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -12,9 +12,12 @@ import 'analyze_command.dart'; import 'build_examples_command.dart'; import 'common/core.dart'; import 'create_all_plugins_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'; @@ -23,9 +26,13 @@ import 'list_command.dart'; import 'make_deps_path_based_command.dart'; import 'native_test_command.dart'; import 'publish_check_command.dart'; -import 'publish_plugin_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'; @@ -50,9 +57,12 @@ void main(List args) { ..addCommand(AnalyzeCommand(packagesDir)) ..addCommand(BuildExamplesCommand(packagesDir)) ..addCommand(CreateAllPluginsAppCommand(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)) @@ -61,9 +71,13 @@ void main(List args) { ..addCommand(NativeTestCommand(packagesDir)) ..addCommand(MakeDepsPathBasedCommand(packagesDir)) ..addCommand(PublishCheckCommand(packagesDir)) - ..addCommand(PublishPluginCommand(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)); diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart index 5ee42848a81b..10abcd44ae6e 100644 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -3,19 +3,20 @@ // found in the LICENSE file. import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/git_version_finder.dart'; -import 'common/plugin_command.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 @@ -23,7 +24,7 @@ const int _exitCannotUpdatePubspec = 4; /// 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 PluginCommand { +class MakeDepsPathBasedCommand extends PackageCommand { /// Creates an instance of the command to convert selected dependencies to /// path-based. MakeDepsPathBasedCommand( @@ -48,6 +49,10 @@ class MakeDepsPathBasedCommand extends PluginCommand { 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'; @@ -73,12 +78,19 @@ class MakeDepsPathBasedCommand extends PluginCommand { final String repoRootPath = (await gitDir).path; for (final File pubspec in await _getAllPubspecs()) { - if (await _addDependencyOverridesIfNecessary( - pubspec, localDependencyPackages)) { - // Print the relative path of the changed pubspec. - final String displayPath = p.posix.joinAll(path - .split(path.relative(pubspec.absolute.path, from: repoRootPath))); - print(' Modified $displayPath'); + 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; } } } @@ -125,23 +137,33 @@ class MakeDepsPathBasedCommand extends PluginCommand { /// If [pubspecFile] has any dependencies on packages in [localDependencies], /// adds dependency_overrides entries to redirect them to the local version /// using path-based dependencies. - /// - /// Returns true if any changes were made. - Future _addDependencyOverridesIfNecessary(File pubspecFile, + 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. 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. + // 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( - 'Plugins with dependency overrides are not currently supported.'); + 'Packages with dependency overrides are not currently supported.'); throw ToolExit(_exitCannotUpdatePubspec); } - final Iterable packagesToOverride = pubspec.dependencies.keys.where( - (String packageName) => localDependencies.containsKey(packageName)); + 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. @@ -155,16 +177,16 @@ class MakeDepsPathBasedCommand extends PluginCommand { // 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 + - ''' + String newPubspecContents = ''' +$pubspecContents -# FOR TESTING ONLY. DO NOT MERGE. +$_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]!.directory.path, + path.relative(localDependencies[packageName]!.path, from: commonBasePath)); newPubspecContents += ''' $packageName: @@ -175,9 +197,9 @@ dependency_overrides: '''; } pubspecFile.writeAsStringSync(newPubspecContents); - return true; + return _RewriteOutcome.changed; } - return false; + return _RewriteOutcome.noChangesNeeded; } /// Returns all pubspecs anywhere under the packages directory. @@ -207,6 +229,10 @@ dependency_overrides: 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. diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index a0d2ebd4e23c..af5f4df98e86 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -198,22 +198,22 @@ this command. Future<_PlatformResult> _testAndroid( RepositoryPackage plugin, _TestMode mode) async { bool exampleHasUnitTests(RepositoryPackage example) { - return example.directory - .childDirectory('android') + return example + .platformDirectory(FlutterPlatform.android) .childDirectory('app') .childDirectory('src') .childDirectory('test') .existsSync() || - example.directory.parent - .childDirectory('android') + plugin + .platformDirectory(FlutterPlatform.android) .childDirectory('src') .childDirectory('test') .existsSync(); } bool exampleHasNativeIntegrationTests(RepositoryPackage example) { - final Directory integrationTestDirectory = example.directory - .childDirectory('android') + final Directory integrationTestDirectory = example + .platformDirectory(FlutterPlatform.android) .childDirectory('app') .childDirectory('src') .childDirectory('androidTest'); @@ -269,7 +269,7 @@ this command. _printRunningExampleTestsMessage(example, 'Android'); final GradleProject project = GradleProject( - example.directory, + example, processRunner: processRunner, platform: platform, ); @@ -406,9 +406,9 @@ this command. ); // The exit code from 'xcodebuild test' when there are no tests. - const int _xcodebuildNoTestExitCode = 66; + const int xcodebuildNoTestExitCode = 66; switch (exitCode) { - case _xcodebuildNoTestExitCode: + case xcodebuildNoTestExitCode: _printNoExampleTestsMessage(example, platform); break; case 0: diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 8fd96b818c1d..14b240dc04c2 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -10,7 +10,6 @@ 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 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; @@ -34,16 +33,13 @@ class PublishCheckCommand extends PackageLoopingCommand { 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.', - defaultsTo: false, ); 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.', - defaultsTo: false, - negatable: true); + ' $_statusMessageError: Some error has occurred.'); } static const String _allowPrereleaseFlag = 'allow-pre-release'; @@ -59,7 +55,7 @@ class PublishCheckCommand extends PackageLoopingCommand { @override final String description = - 'Checks to make sure that a plugin *could* be published.'; + 'Checks to make sure that a package *could* be published.'; final PubVersionFinder _pubVersionFinder; @@ -134,7 +130,23 @@ class PublishCheckCommand extends PackageLoopingCommand { } } + // 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, @@ -247,12 +259,12 @@ HTTP response: ${pubVersionFinderResponse.httpResponse.body} bool _passesAuthorsCheck(RepositoryPackage package) { final List pathComponents = - package.directory.fileSystem.path.split(package.directory.path); + 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.directory.childFile('AUTHORS').existsSync(); + return package.authorsFile.existsSync(); } void _printImportantStatusMessage(String message, {required bool isError}) { diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_command.dart similarity index 95% rename from script/tool/lib/src/publish_plugin_command.dart rename to script/tool/lib/src/publish_command.dart index 28d17a3a2487..e7b3d110c5fa 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_command.dart @@ -13,14 +13,13 @@ 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:pubspec_parse/pubspec_parse.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/plugin_command.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; import 'common/repository_package.dart'; @@ -43,13 +42,13 @@ class _RemoteInfo { /// 2. Tags the release with the format -v. /// 3. Pushes the release to a remote. /// -/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full +/// 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 PublishPluginCommand extends PackageLoopingCommand { +class PublishCommand extends PackageLoopingCommand { /// Creates an instance of the publish command. - PublishPluginCommand( + PublishCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), @@ -75,7 +74,6 @@ class PublishPluginCommand extends PackageLoopingCommand { 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.', - defaultsTo: false, ); argParser.addFlag( _dryRunFlag, @@ -83,14 +81,10 @@ class PublishPluginCommand extends PackageLoopingCommand { '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`', - defaultsTo: false, - negatable: true, ); 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', - defaultsTo: false, - negatable: true); + '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'; @@ -106,7 +100,7 @@ class PublishPluginCommand extends PackageLoopingCommand { static const String _tagFormat = '%PACKAGE%-v%VERSION%'; @override - final String name = 'publish-plugin'; + final String name = 'publish'; @override final String description = @@ -122,6 +116,8 @@ class PublishPluginCommand extends PackageLoopingCommand { 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'; @@ -150,6 +146,11 @@ class PublishPluginCommand extends PackageLoopingCommand { _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 ==============='); } @@ -334,22 +335,18 @@ Safe to ignore if the package is deleted in this commit. Future _publish(RepositoryPackage package) async { print('Publishing...'); - final List publishFlags = getStringListArg(_pubFlagsOption); - print('Running `pub publish ${publishFlags.join(' ')}` in ' + print('Running `pub publish ${_publishFlags.join(' ')}` in ' '${package.directory.absolute.path}...\n'); if (getBoolArg(_dryRunFlag)) { return true; } - if (getBoolArg(_skipConfirmationFlag)) { - publishFlags.add('--force'); - } - if (publishFlags.contains('--force')) { + if (_publishFlags.contains('--force')) { _ensureValidPubCredential(); } final io.Process publish = await processRunner.start( - flutterCommand, ['pub', 'publish'] + publishFlags, + 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)); diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 2c27c91e0490..79ef1e1d3e5e 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -5,7 +5,6 @@ import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:platform/platform.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; import 'common/core.dart'; @@ -65,7 +64,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { bool get hasLongOutput => false; @override - bool get includeSubpackages => true; + PackageLoopingType get packageLoopingType => + PackageLoopingType.includeAllSubpackages; @override Future runForPackage(RepositoryPackage package) async { @@ -191,6 +191,11 @@ class PubspecCheckCommand extends PackageLoopingCommand { 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) { @@ -226,8 +231,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { bool _checkIssueLink(Pubspec pubspec) { return pubspec.issueTracker ?.toString() - .startsWith(_expectedIssueLinkFormat) == - true; + .startsWith(_expectedIssueLinkFormat) ?? + false; } // Validates the "implements" keyword for a plugin, returning an error @@ -288,8 +293,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { .where((String package) => !dependencies.contains(package)); if (missingPackages.isNotEmpty) { return 'The following default_packages are missing ' - 'corresponding dependencies:\n ' + - missingPackages.join('\n '); + 'corresponding dependencies:\n' + ' ${missingPackages.join('\n ')}'; } return null; diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart new file mode 100644 index 000000000000..e3fbc7bc454d --- /dev/null +++ b/script/tool/lib/src/readme_check_command.dart @@ -0,0 +1,343 @@ +// 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 dynamic platformsEntry = pubspec.flutter!['plugin']!['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 new file mode 100644 index 000000000000..3085e0df85e0 --- /dev/null +++ b/script/tool/lib/src/remove_dev_dependencies.dart @@ -0,0 +1,58 @@ +// 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 index 2c5dd9934b45..5101b8f19e7e 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -36,14 +36,18 @@ class TestCommand extends PackageLoopingCommand { 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.directory.childDirectory('test').existsSync()) { + if (!package.testDirectory.existsSync()) { return PackageResult.skip('No test/ directory.'); } bool passed; - if (isFlutterPackage(package.directory)) { + if (package.requiresFlutter()) { passed = await _runFlutterTests(package); } else { passed = await _runDartTests(package); @@ -88,7 +92,6 @@ class TestCommand extends PackageLoopingCommand { exitCode = await processRunner.runAndStream( 'dart', [ - 'pub', 'run', if (experiment.isNotEmpty) '--enable-experiment=$experiment', 'test', diff --git a/script/tool/lib/src/update_excerpts_command.dart b/script/tool/lib/src/update_excerpts_command.dart new file mode 100644 index 000000000000..5a59104d4e7f --- /dev/null +++ b/script/tool/lib/src/update_excerpts_command.dart @@ -0,0 +1,225 @@ +// 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 unless nothing has + /// changed. + Future _validateRepositoryState() async { + final io.ProcessResult modifiedFiles = await processRunner.run( + 'git', + ['ls-files', '--modified'], + workingDir: packagesDir, + logOnError: true, + ); + if (modifiedFiles.exitCode != 0) { + return 'Unable to determine local file state'; + } + + final String stdout = modifiedFiles.stdout as String; + return stdout.trim().isEmpty ? null : 'Snippets are out of sync'; + } +} diff --git a/script/tool/lib/src/update_release_info_command.dart b/script/tool/lib/src/update_release_info_command.dart new file mode 100644 index 000000000000..67aa994d963c --- /dev/null +++ b/script/tool/lib/src/update_release_info_command.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 '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) { + 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 index fcaea335920f..b3be672066d9 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -9,17 +9,15 @@ 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:pubspec_parse/pubspec_parse.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'; -const int _exitMissingChangeDescriptionFile = 3; - /// Categories of version change types. enum NextVersionType { /// A breaking change. @@ -31,8 +29,8 @@ enum NextVersionType { /// A bugfix change. PATCH, - /// The release of an existing prerelease version. - RELEASE, + /// The release of an existing pre-1.0 version. + V1_RELEASE, } /// The state of a package's version relative to the comparison base. @@ -40,8 +38,11 @@ enum _CurrentVersionState { /// The version is unchanged. unchanged, - /// The version has changed, and the transition is valid. - validChange, + /// 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, @@ -50,8 +51,8 @@ enum _CurrentVersionState { unknown, } -/// Returns the set of allowed next versions, with their change type, for -/// [version]. +/// 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. @@ -75,17 +76,17 @@ Map getAllowedNextVersions( final int currentBuildNumber = version.build.first as int; nextBuildNumber = currentBuildNumber + 1; } - final Version preReleaseVersion = Version( + final Version nextBuildVersion = Version( version.major, version.minor, version.patch, build: nextBuildNumber.toString(), ); allowedNextVersions.clear(); - allowedNextVersions[version.nextMajor] = NextVersionType.RELEASE; + allowedNextVersions[version.nextMajor] = NextVersionType.V1_RELEASE; allowedNextVersions[version.nextMinor] = NextVersionType.BREAKING_MAJOR; allowedNextVersions[version.nextPatch] = NextVersionType.MINOR; - allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; + allowedNextVersions[nextBuildVersion] = NextVersionType.PATCH; } return allowedNextVersions; } @@ -112,55 +113,49 @@ class VersionCheckCommand extends PackageLoopingCommand { 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.', - defaultsTo: false, - negatable: true, ); - argParser.addOption(_changeDescriptionFile, - help: 'The path to a file containing the description of the change ' - '(e.g., PR description or commit message).\n\n' - 'If supplied, this is used to allow overrides to some version ' + 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 --$_changeDescriptionFile, this is should only be ' - 'used in pre-submit CI checks, to prevent the possibility of ' - 'post-submit breakage if an override justification is not ' - 'transferred into the commit message.', + '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 the possibility of post-submit breakage if a change ' - 'description justification is not transferred into the commit ' - 'message. Pre-submit checks should always use ' - '--$_changeDescriptionFile instead.', + '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 _changeDescriptionFile = 'change-description-file'; + static const String _prLabelsArg = 'pr-labels'; static const String _checkForMissingChanges = 'check-for-missing-changes'; static const String _ignorePlatformInterfaceBreaks = 'ignore-platform-interface-breaks'; - /// The string that must be in [_changeDescriptionFile] to allow a breaking + /// The label that must be on a PR to allow a breaking /// change to a platform interface. - static const String _breakingChangeJustificationMarker = - '## Breaking change justification'; + static const String _breakingChangeOverrideLabel = + 'override: allow breaking change'; - /// The string that must be at the start of a line in [_changeDescriptionFile] - /// to allow skipping a version change for a PR that would normally require - /// one. - static const String _missingVersionChangeJustificationMarker = - 'No version 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 string that must be at the start of a line in [_changeDescriptionFile] - /// to allow skipping a CHANGELOG change for a PR that would normally require - /// one. - static const String _missingChangelogChangeJustificationMarker = - 'No CHANGELOG change:'; + /// 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; @@ -168,14 +163,14 @@ class VersionCheckCommand extends PackageLoopingCommand { late final String _mergeBase; late final List _changedFiles; - late final String _changeDescription = _loadChangeDescription(); + late final Set _prLabels = _getPRLabels(); @override final String name = 'version-check'; @override final String description = - 'Checks if the versions of the plugins have been incremented per pub specification.\n' + '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.'; @@ -219,7 +214,8 @@ class VersionCheckCommand extends PackageLoopingCommand { case _CurrentVersionState.unchanged: versionChanged = false; break; - case _CurrentVersionState.validChange: + case _CurrentVersionState.validIncrease: + case _CurrentVersionState.validRevert: versionChanged = true; break; case _CurrentVersionState.invalidChange: @@ -233,7 +229,7 @@ class VersionCheckCommand extends PackageLoopingCommand { } if (!(await _validateChangelogVersion(package, - pubspec: pubspec, pubspecVersionChanged: versionChanged))) { + pubspec: pubspec, pubspecVersionState: versionState))) { errors.add('CHANGELOG.md failed validation.'); } @@ -322,8 +318,8 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} print('${indentation}Unable to find previous version ' '${getBoolArg(_againstPubFlag) ? 'on pub server' : 'at git base'}.'); logWarning( - '${indentation}If this plugin is not new, something has gone wrong.'); - return _CurrentVersionState.validChange; // Assume new, thus valid. + '${indentation}If this package is not new, something has gone wrong.'); + return _CurrentVersionState.validIncrease; // Assume new, thus valid. } if (previousVersion == currentVersion) { @@ -333,22 +329,22 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} // Check for reverts when doing local validation. if (!getBoolArg(_againstPubFlag) && currentVersion < previousVersion) { - final Map possibleVersionsFromNewVersion = - getAllowedNextVersions(currentVersion, newVersion: 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 (possibleVersionsFromNewVersion.containsKey(previousVersion)) { + if (_shouldAllowVersionChange( + oldVersion: currentVersion, newVersion: previousVersion)) { logWarning('${indentation}New version is lower than previous version. ' 'This is assumed to be a revert.'); - return _CurrentVersionState.validChange; + return _CurrentVersionState.validRevert; } } final Map allowedNextVersions = getAllowedNextVersions(previousVersion, newVersion: currentVersion); - if (allowedNextVersions.containsKey(currentVersion)) { + if (_shouldAllowVersionChange( + oldVersion: previousVersion, newVersion: currentVersion)) { print('$indentation$previousVersion -> $currentVersion'); } else { printError('${indentation}Incorrectly updated version.\n' @@ -357,7 +353,13 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} return _CurrentVersionState.invalidChange; } - if (allowedNextVersions[currentVersion] == NextVersionType.BREAKING_MAJOR && + // 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 ' @@ -368,7 +370,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} return _CurrentVersionState.invalidChange; } - return _CurrentVersionState.validChange; + return _CurrentVersionState.validIncrease; } /// Checks whether or not [package]'s CHANGELOG's versioning is correct, @@ -379,13 +381,13 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} Future _validateChangelogVersion( RepositoryPackage package, { required Pubspec pubspec, - required bool pubspecVersionChanged, + 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.directory.childFile('CHANGELOG.md'); + final File changelog = package.changelogFile; final List lines = changelog.readAsLinesSync(); String? firstLineWithText; final Iterator iterator = lines.iterator; @@ -400,14 +402,15 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} final String badNextErrorMessage = '${indentation}When bumping the version ' 'for release, the NEXT section should be incorporated into the new ' - 'version\'s release notes.'; + "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 changes the version. - if (pubspecVersionChanged) { + // NEXT should not be present in a commit that increases the version. + if (pubspecVersionState == _CurrentVersionState.validIncrease || + pubspecVersionState == _CurrentVersionState.invalidChange) { printError(badNextErrorMessage); return false; } @@ -487,32 +490,45 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. return true; } - if (_getChangeDescription().contains(_breakingChangeJustificationMarker)) { + if (_prLabels.contains(_breakingChangeOverrideLabel)) { logWarning( '${indentation}Allowing breaking change to ${package.displayName} ' - 'due to "$_breakingChangeJustificationMarker" in the change ' - 'description.'); + 'due to the "$_breakingChangeOverrideLabel" label.'); return true; } return false; } - String _getChangeDescription() => _changeDescription; + /// 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 the contents of the file pointed to by [_changeDescriptionFile], - /// or an empty string if that flag is not provided. - String _loadChangeDescription() { - final String path = getStringArg(_changeDescriptionFile); - if (path.isEmpty) { - return ''; + /// 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; } - final File file = packagesDir.fileSystem.file(path); - if (!file.existsSync()) { - printError('${indentation}No such file: $path'); - throw ToolExit(_exitMissingChangeDescriptionFile); + // 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 file.readAsStringSync(); + return false; } /// Returns an error string if the changes to this package should have @@ -527,74 +543,46 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. final Directory gitRoot = packagesDir.fileSystem.directory((await gitDir).path); final String relativePackagePath = - getRelativePosixPath(package.directory, from: gitRoot) + '/'; - bool hasChanges = false; - bool needsVersionChange = false; - bool hasChangelogChange = false; - for (final String path in _changedFiles) { - // Only consider files within the package. - if (!path.startsWith(relativePackagePath)) { - continue; - } - hasChanges = true; + getRelativePosixPath(package.directory, from: gitRoot); - final List components = p.posix.split(path); - final bool isChangelog = components.last == 'CHANGELOG.md'; - if (isChangelog) { - hasChangelogChange = true; - } - - if (!needsVersionChange && - !isChangelog && - // The example's main.dart is shown on pub.dev, but for anything else - // in the example publishing has no purpose. - !(components.contains('example') && components.last != 'main.dart') && - // Changes to tests don't need to be published. - !components.contains('test') && - !components.contains('androidTest') && - !components.contains('RunnerTests') && - !components.contains('RunnerUITests') && - // Ignoring lints doesn't affect clients. - !components.contains('lint-baseline.xml')) { - needsVersionChange = true; - } - } + final PackageChangeState state = await checkPackageChangeState(package, + changedPaths: _changedFiles, + relativePackagePath: relativePackagePath, + git: await retrieveVersionFinder()); - if (!hasChanges) { + if (!state.hasChanges) { return null; } - if (needsVersionChange) { - if (_getChangeDescription().split('\n').any((String line) => - line.startsWith(_missingVersionChangeJustificationMarker))) { - logWarning('Ignoring lack of version change due to ' - '"$_missingVersionChangeJustificationMarker" in the ' - 'change description.'); + 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 ' - 'add a line starting with\n' - '$_missingVersionChangeJustificationMarker\n' - 'to your PR description with an explanation of why it is exempt.'); + '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 (!hasChangelogChange) { - if (_getChangeDescription().split('\n').any((String line) => - line.startsWith(_missingChangelogChangeJustificationMarker))) { - logWarning('Ignoring lack of CHANGELOG update due to ' - '"$_missingChangelogChangeJustificationMarker" in the ' - 'change description.'); + 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' + 'No CHANGELOG change found. If this PR needs an exemption from ' 'the standard policy of listing all changes in the CHANGELOG, ' - 'please add a line starting with\n' - '$_missingChangelogChangeJustificationMarker\n' - 'to your PR description with an explanation of why.'); + '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'; } } diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart index 4298acb1c7e5..a81bf15477af 100644 --- a/script/tool/lib/src/xcode_analyze_command.dart +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -23,8 +23,20 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { 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 @@ -57,15 +69,24 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { 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' + 'generic/platform=iOS Simulator', + if (minIOSVersion.isNotEmpty) + 'IPHONEOS_DEPLOYMENT_TARGET=$minIOSVersion', ])) { failures.add('iOS'); } - if (testMacOS && !await _analyzePlugin(package, 'macOS')) { + if (testMacOS && + !await _analyzePlugin(package, 'macOS', extraFlags: [ + if (minMacOSVersion.isNotEmpty) + 'MACOSX_DEPLOYMENT_TARGET=$minMacOSVersion', + ])) { failures.add('macOS'); } diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 9ca5e2b77580..eecff3703b4c 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ 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.8.0 +version: 0.12.1 dependencies: args: ^2.1.0 @@ -21,6 +21,7 @@ dependencies: test: ^1.17.3 uuid: ^3.0.4 yaml: ^3.1.0 + yaml_edit: ^2.0.2 dev_dependencies: build_runner: ^2.0.3 diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 878facd83c06..e6b910960846 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -37,43 +37,45 @@ void main() { }); test('analyzes all packages', () async { - final Directory plugin1Dir = createFakePlugin('a', packagesDir); - final Directory plugin2Dir = createFakePlugin('b', packagesDir); + final RepositoryPackage package1 = createFakePackage('a', packagesDir); + final RepositoryPackage plugin2 = createFakePlugin('b', packagesDir); await runCapturingPrint(runner, ['analyze']); expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'flutter', const ['packages', 'get'], plugin1Dir.path), - ProcessCall( - 'flutter', const ['packages', 'get'], plugin2Dir.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin1Dir.path), + ProcessCall('flutter', const ['pub', 'get'], package1.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin2Dir.path), + 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 Directory plugin1Dir = createFakePlugin('a', packagesDir); + final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); await runCapturingPrint(runner, ['analyze']); expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall('flutter', const ['pub', 'get'], plugin1.path), ProcessCall( - 'flutter', const ['packages', 'get'], plugin1Dir.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin1Dir.path), + 'dart', const ['analyze', '--fatal-infos'], plugin1.path), ])); }); - test('don\'t elide a non-contained example package', () async { - final Directory plugin1Dir = createFakePlugin('a', packagesDir); - final Directory plugin2Dir = createFakePlugin('example', packagesDir); + 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']); @@ -81,18 +83,89 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - 'flutter', const ['packages', 'get'], plugin1Dir.path), + 'flutter', const ['pub', 'get'], mainPackage.path), ProcessCall( - 'flutter', const ['packages', 'get'], plugin2Dir.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin1Dir.path), + 'flutter', const ['pub', 'get'], subpackage1.path), + ProcessCall( + 'flutter', const ['pub', 'get'], subpackage2.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin2Dir.path), + 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 Directory pluginDir = createFakePlugin('a', packagesDir); + final RepositoryPackage plugin = createFakePlugin('a', packagesDir); await runCapturingPrint( runner, ['analyze', '--analysis-sdk', 'foo/bar/baz']); @@ -102,13 +175,40 @@ void main() { orderedEquals([ ProcessCall( 'flutter', - const ['packages', 'get'], - pluginDir.path, + const ['pub', 'get'], + plugin.path, ), ProcessCall( 'foo/bar/baz/bin/dart', const ['analyze', '--fatal-infos'], - pluginDir.path, + 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, ), ]), ); @@ -160,7 +260,7 @@ void main() { }); test('takes an allow list', () async { - final Directory pluginDir = createFakePlugin('foo', packagesDir, + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, extraFiles: ['analysis_options.yaml']); await runCapturingPrint( @@ -169,15 +269,14 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'flutter', const ['packages', 'get'], pluginDir.path), + ProcessCall('flutter', const ['pub', 'get'], plugin.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], - pluginDir.path), + plugin.path), ])); }); test('takes an allow config file', () async { - final Directory pluginDir = createFakePlugin('foo', packagesDir, + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, extraFiles: ['analysis_options.yaml']); final File allowFile = packagesDir.childFile('custom.yaml'); allowFile.writeAsStringSync('- foo'); @@ -188,13 +287,24 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - 'flutter', const ['packages', 'get'], pluginDir.path), + ProcessCall('flutter', const ['pub', 'get'], plugin.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], - pluginDir.path), + 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, @@ -207,11 +317,11 @@ void main() { }); }); - test('fails if "packages get" fails', () async { + test('fails if "pub get" fails', () async { createFakePlugin('foo', packagesDir); processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(exitCode: 1) // flutter packages get + MockProcess(exitCode: 1) // flutter pub get ]; Error? commandError; @@ -229,6 +339,28 @@ void main() { ); }); + 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); @@ -260,7 +392,7 @@ void main() { // 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 Directory pluginDir = createFakePlugin('foo', packagesDir, + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, extraFiles: ['analysis_options.yaml']); final File allowFile = packagesDir.childFile('custom.yaml'); allowFile.writeAsStringSync('- foo'); @@ -279,13 +411,13 @@ void main() { orderedEquals([ ProcessCall( 'flutter', - const ['packages', 'get'], - pluginDir.path, + const ['pub', 'get'], + plugin.path, ), ProcessCall( 'foo/bar/baz/bin/dart', const ['analyze', '--fatal-infos'], - pluginDir.path, + plugin.path, ), ]), ); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 6d8f0b9d6486..a819e7a12674 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -63,7 +63,7 @@ void main() { processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ - MockProcess(exitCode: 1) // flutter packages get + MockProcess(exitCode: 1) // flutter pub get ]; Error? commandError; @@ -92,7 +92,7 @@ void main() { processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ - MockProcess(exitCode: 1) // flutter packages get + MockProcess(exitCode: 1) // flutter pub get ]; Error? commandError; @@ -134,13 +134,12 @@ void main() { test('building for iOS', () async { mockPlatform.isMacOS = true; - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformIOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, ['build-examples', '--ios', '--enable-experiment=exp1']); @@ -191,13 +190,12 @@ void main() { test('building for Linux', () async { mockPlatform.isLinux = true; - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint( runner, ['build-examples', '--linux']); @@ -240,13 +238,12 @@ void main() { test('building for macOS', () async { mockPlatform.isMacOS = true; - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint( runner, ['build-examples', '--macos']); @@ -286,13 +283,12 @@ void main() { }); test('building for web', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformWeb: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, ['build-examples', '--web']); @@ -313,7 +309,7 @@ void main() { }); test( - 'building for win32 when plugin is not set up for Windows results in no-op', + 'building for Windows when plugin is not set up for Windows results in no-op', () async { mockPlatform.isWindows = true; createFakePlugin('plugin', packagesDir); @@ -325,7 +321,7 @@ void main() { output, containsAllInOrder([ contains('Running for plugin'), - contains('Win32 is not supported by this plugin'), + contains('Windows is not supported by this plugin'), ]), ); @@ -334,15 +330,14 @@ void main() { expect(processRunner.recordedCalls, orderedEquals([])); }); - test('building for win32', () async { + test('building for Windows', () async { mockPlatform.isWindows = true; - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint( runner, ['build-examples', '--windows']); @@ -350,7 +345,7 @@ void main() { expect( output, containsAllInOrder([ - '\nBUILDING plugin/example for Win32 (windows)', + '\nBUILDING plugin/example for Windows', ]), ); @@ -364,88 +359,6 @@ void main() { ])); }); - test('building for UWP when plugin does not support UWP is a no-op', - () async { - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--winuwp']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('UWP 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 UWP', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.federated, - variants: [platformVariantWinUwp]), - }); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--winuwp']); - - expect( - output, - containsAllInOrder([ - contains('BUILDING plugin/example for UWP (winuwp)'), - ]), - ); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'winuwp'], pluginExampleDirectory.path), - ])); - }); - - test('building for UWP creates a folder if necessary', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.federated, - variants: [platformVariantWinUwp]), - }); - - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--winuwp']); - - expect( - output, - contains('Creating temporary winuwp folder'), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['create', '--platforms=winuwp', '.'], - pluginExampleDirectory.path), - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'winuwp'], pluginExampleDirectory.path), - ])); - }); - test( 'building for Android when plugin is not set up for Android results in no-op', () async { @@ -468,13 +381,12 @@ void main() { }); test('building for Android', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'build-examples', @@ -497,13 +409,12 @@ void main() { }); test('enable-experiment flag for Android', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); await runCapturingPrint(runner, ['build-examples', '--apk', '--enable-experiment=exp1']); @@ -519,13 +430,12 @@ void main() { }); test('enable-experiment flag for ios', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformIOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); await runCapturingPrint(runner, ['build-examples', '--ios', '--enable-experiment=exp1']); @@ -563,7 +473,7 @@ void main() { group('packages', () { test('builds when requested platform is supported by example', () async { - final Directory packageDirectory = createFakePackage( + final RepositoryPackage package = createFakePackage( 'package', packagesDir, isFlutter: true, extraFiles: [ 'example/ios/Runner.xcodeproj/project.pbxproj' ]); @@ -589,12 +499,12 @@ void main() { 'ios', '--no-codesign', ], - packageDirectory.childDirectory('example').path), + getExampleDir(package).path), ])); }); test('skips non-Flutter examples', () async { - createFakePackage('package', packagesDir, isFlutter: false); + createFakePackage('package', packagesDir); final List output = await runCapturingPrint( runner, ['build-examples', '--ios']); @@ -649,7 +559,7 @@ void main() { }); test('logs skipped platforms when only some are supported', () async { - final Directory packageDirectory = createFakePackage( + final RepositoryPackage package = createFakePackage( 'package', packagesDir, isFlutter: true, extraFiles: ['example/linux/CMakeLists.txt']); @@ -672,21 +582,20 @@ void main() { ProcessCall( getFlutterCommand(mockPlatform), const ['build', 'linux'], - packageDirectory.childDirectory('example').path), + getExampleDir(package).path), ])); }); }); test('The .pluginToolsConfig.yaml file', () async { mockPlatform.isLinux = true; - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final File pluginExampleConfigFile = pluginExampleDirectory.childFile('.pluginToolsConfig.yaml'); diff --git a/script/tool/test/common/file_utils_test.dart b/script/tool/test/common/file_utils_test.dart index e3986842a969..79b804e31ea5 100644 --- a/script/tool/test/common/file_utils_test.dart +++ b/script/tool/test/common/file_utils_test.dart @@ -10,7 +10,7 @@ import 'package:test/test.dart'; void main() { test('works on Posix', () async { final FileSystem fileSystem = - MemoryFileSystem(style: FileSystemStyle.posix); + MemoryFileSystem(); final Directory base = fileSystem.directory('/').childDirectory('base'); final File file = diff --git a/script/tool/test/common/git_version_finder_test.dart b/script/tool/test/common/git_version_finder_test.dart index ad1a26ffc165..d5a5dd4fe876 100644 --- a/script/tool/test/common/git_version_finder_test.dart +++ b/script/tool/test/common/git_version_finder_test.dart @@ -8,7 +8,7 @@ import 'package:flutter_plugin_tools/src/common/git_version_finder.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import 'plugin_command_test.mocks.dart'; +import 'package_command_test.mocks.dart'; void main() { late List?> gitDirCommands; diff --git a/script/tool/test/common/gradle_test.dart b/script/tool/test/common/gradle_test.dart index 3eac60baf3c3..8df4a65b93a5 100644 --- a/script/tool/test/common/gradle_test.dart +++ b/script/tool/test/common/gradle_test.dart @@ -23,7 +23,7 @@ void main() { group('isConfigured', () { test('reports true when configured on Windows', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/gradlew.bat']); final GradleProject project = GradleProject( @@ -36,7 +36,7 @@ void main() { }); test('reports true when configured on non-Windows', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/gradlew']); final GradleProject project = GradleProject( @@ -49,7 +49,7 @@ void main() { }); test('reports false when not configured on Windows', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/foo']); final GradleProject project = GradleProject( @@ -62,7 +62,7 @@ void main() { }); test('reports true when configured on non-Windows', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/foo']); final GradleProject project = GradleProject( @@ -75,9 +75,9 @@ void main() { }); }); - group('runXcodeBuild', () { + group('runCommand', () { test('runs without arguments', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/gradlew']); final GradleProject project = GradleProject( @@ -93,16 +93,19 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - plugin.childDirectory('android').childFile('gradlew').path, + plugin + .platformDirectory(FlutterPlatform.android) + .childFile('gradlew') + .path, const [ 'foo', ], - plugin.childDirectory('android').path), + plugin.platformDirectory(FlutterPlatform.android).path), ])); }); test('runs with arguments', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/gradlew']); final GradleProject project = GradleProject( @@ -121,18 +124,21 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - plugin.childDirectory('android').childFile('gradlew').path, + plugin + .platformDirectory(FlutterPlatform.android) + .childFile('gradlew') + .path, const [ 'foo', '--bar', '--baz', ], - plugin.childDirectory('android').path), + plugin.platformDirectory(FlutterPlatform.android).path), ])); }); test('runs with the correct wrapper on Windows', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/gradlew.bat']); final GradleProject project = GradleProject( @@ -148,16 +154,19 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall( - plugin.childDirectory('android').childFile('gradlew.bat').path, + plugin + .platformDirectory(FlutterPlatform.android) + .childFile('gradlew.bat') + .path, const [ 'foo', ], - plugin.childDirectory('android').path), + plugin.platformDirectory(FlutterPlatform.android).path), ])); }); test('returns error codes', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', fileSystem.directory('/'), extraFiles: ['android/gradlew.bat']); final GradleProject project = GradleProject( diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/package_command_test.dart similarity index 71% rename from script/tool/test/common/plugin_command_test.dart rename to script/tool/test/common/package_command_test.dart index 28a03c61d59f..aa0a20253955 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/package_command_test.dart @@ -8,7 +8,7 @@ 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_command.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'; @@ -18,12 +18,12 @@ import 'package:test/test.dart'; import '../mocks.dart'; import '../util.dart'; -import 'plugin_command_test.mocks.dart'; +import 'package_command_test.mocks.dart'; @GenerateMocks([GitDir]) void main() { late RecordingProcessRunner processRunner; - late SamplePluginCommand command; + late SamplePackageCommand command; late CommandRunner runner; late FileSystem fileSystem; late MockPlatform mockPlatform; @@ -49,7 +49,7 @@ void main() { return processRunner.run('git-$gitCommand', arguments); }); processRunner = RecordingProcessRunner(); - command = SamplePluginCommand( + command = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, @@ -62,18 +62,24 @@ void main() { group('plugin iteration', () { test('all plugins from file system', () async { - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + 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 Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - final Directory package3 = createFakePackage('package3', packagesDir); - final Directory package4 = createFakePackage('package4', packagesDir); + 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, @@ -85,10 +91,25 @@ void main() { ])); }); + 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 Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - final Directory plugin3 = + 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, @@ -96,10 +117,12 @@ void main() { }); test('--packages limits packages', () async { - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); createFakePackage('package3', packagesDir); - final Directory package4 = createFakePackage('package4', packagesDir); + final RepositoryPackage package4 = + createFakePackage('package4', packagesDir); await runCapturingPrint( runner, ['sample', '--packages=plugin1,package4']); expect( @@ -111,10 +134,12 @@ void main() { }); test('--plugins acts as an alias to --packages', () async { - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); createFakePackage('package3', packagesDir); - final Directory package4 = createFakePackage('package4', packagesDir); + final RepositoryPackage package4 = + createFakePackage('package4', packagesDir); await runCapturingPrint( runner, ['sample', '--plugins=plugin1,package4']); expect( @@ -127,7 +152,8 @@ void main() { test('exclude packages when packages flag is specified', () async { createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ 'sample', '--packages=plugin1,plugin2', @@ -136,7 +162,7 @@ void main() { expect(command.plugins, unorderedEquals([plugin2.path])); }); - test('exclude packages when packages flag isn\'t specified', () async { + test("exclude packages when packages flag isn't specified", () async { createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); await runCapturingPrint( @@ -146,7 +172,8 @@ void main() { test('exclude federated plugins when packages flag is specified', () async { createFakePlugin('plugin1', packagesDir.childDirectory('federated')); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ 'sample', '--packages=federated/plugin1,plugin2', @@ -158,7 +185,8 @@ void main() { test('exclude entire federated plugins when packages flag is specified', () async { createFakePlugin('plugin1', packagesDir.childDirectory('federated')); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, [ 'sample', '--packages=federated/plugin1,plugin2', @@ -189,11 +217,11 @@ packages/plugin1/plugin1/plugin1.dart '''), ]; final Directory pluginGroup = packagesDir.childDirectory('plugin1'); - final Directory appFacingPackage = + final RepositoryPackage appFacingPackage = createFakePlugin('plugin1', pluginGroup); - final Directory platformInterfacePackage = + final RepositoryPackage platformInterfacePackage = createFakePlugin('plugin1_platform_interface', pluginGroup); - final Directory implementationPackage = + final RepositoryPackage implementationPackage = createFakePlugin('plugin1_web', pluginGroup); await runCapturingPrint( @@ -217,7 +245,7 @@ packages/plugin1/plugin1/plugin1.dart '''), ]; final Directory pluginGroup = packagesDir.childDirectory('plugin1'); - final Directory appFacingPackage = + final RepositoryPackage appFacingPackage = createFakePlugin('plugin1', pluginGroup); createFakePlugin('plugin1_platform_interface', pluginGroup); createFakePlugin('plugin1_web', pluginGroup); @@ -239,7 +267,7 @@ packages/plugin1/plugin1/plugin1.dart final Directory pluginGroup = packagesDir.childDirectory('plugin1'); createFakePlugin('plugin1', pluginGroup); - final Directory platformInterfacePackage = + final RepositoryPackage platformInterfacePackage = createFakePlugin('plugin1_platform_interface', pluginGroup); createFakePlugin('plugin1_web', pluginGroup); @@ -253,6 +281,30 @@ packages/plugin1/plugin1/plugin1.dart 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 { @@ -317,8 +369,10 @@ packages/plugin1/plugin1/plugin1.dart group('test run-on-changed-packages', () { test('all plugins should be tested if there are no changes.', () async { - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -332,8 +386,10 @@ packages/plugin1/plugin1/plugin1.dart processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: 'AUTHORS'), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -348,13 +404,22 @@ packages/plugin1/plugin1/plugin1.dart packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, + 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 { @@ -364,13 +429,21 @@ packages/plugin1/CHANGELOG packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, + 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', @@ -381,16 +454,24 @@ packages/plugin1/CHANGELOG packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, + 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.', + test('all plugins should be tested if anything in script/ changes.', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: ''' @@ -398,13 +479,21 @@ script/tool_runner.sh packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, + 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.', @@ -415,13 +504,21 @@ analysis_options.yaml packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, + 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.', @@ -432,20 +529,29 @@ packages/plugin1/CHANGELOG packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, + 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 Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -454,7 +560,7 @@ packages/plugin1/CHANGELOG output, containsAllInOrder([ contains( - 'Running for all packages that have changed relative to "main"'), + 'Running for all packages that have diffs relative to "main"'), ])); expect(command.plugins, unorderedEquals([plugin1.path])); @@ -468,7 +574,8 @@ packages/plugin1/plugin1.dart packages/plugin1/ios/plugin1.m '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); await runCapturingPrint(runner, ['sample', '--base-sha=main', '--run-on-changed-packages']); @@ -484,8 +591,10 @@ packages/plugin1/plugin1.dart packages/plugin2/ios/plugin2.m '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + 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']); @@ -504,7 +613,7 @@ packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart packages/plugin1/plugin1_web/plugin1_web.dart '''), ]; - final Directory plugin1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); @@ -522,7 +631,7 @@ packages/plugin1/plugin1_web/plugin1_web.dart packages/plugin1/plugin1/plugin1.dart '''), ]; - final Directory plugin1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin1_platform_interface', packagesDir.childDirectory('plugin1')); @@ -541,7 +650,7 @@ packages/plugin2/ios/plugin2.m packages/plugin3/plugin3.dart '''), ]; - final Directory plugin1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); createFakePlugin('plugin2', packagesDir); createFakePlugin('plugin3', packagesDir); @@ -601,7 +710,8 @@ script/tool_runner.sh processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: 'packages/a_package/lib/a_package.dart'), ]; - final Directory packageA = createFakePackage('a_package', packagesDir); + final RepositoryPackage packageA = + createFakePackage('a_package', packagesDir); createFakePlugin('b_package', packagesDir); final List output = await runCapturingPrint( runner, ['sample', '--run-on-dirty-packages']); @@ -624,8 +734,10 @@ packages/a_package/lib/a_package.dart packages/b_package/lib/src/foo.dart '''), ]; - final Directory packageA = createFakePackage('a_package', packagesDir); - final Directory packageB = createFakePackage('b_package', packagesDir); + 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']); @@ -641,7 +753,8 @@ packages/a_package/lib/a_package.dart packages/b_package/lib/src/foo.dart '''), ]; - final Directory packageA = createFakePackage('a_package', packagesDir); + final RepositoryPackage packageA = + createFakePackage('a_package', packagesDir); createFakePackage('b_package', packagesDir); createFakePackage('c_package', packagesDir); await runCapturingPrint(runner, [ @@ -656,14 +769,19 @@ packages/b_package/lib/src/foo.dart }); group('--packages-for-branch', () { - test('only tests changed packages on a branch', () async { + 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'), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + processRunner.mockProcessesForExecutable['git-merge-base'] = [ + MockProcess(stdout: 'abc123'), + ]; + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint( @@ -673,30 +791,49 @@ packages/b_package/lib/src/foo.dart expect( output, containsAllInOrder([ - contains('--packages-for-branch: running on changed packages'), + 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('tests all packages on main', () async { + 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 Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + 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, plugin2.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); expect( output, containsAllInOrder([ - contains('--packages-for-branch: running on all packages'), + 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 { @@ -706,19 +843,29 @@ packages/b_package/lib/src/foo.dart processRunner.mockProcessesForExecutable['git-rev-parse'] = [ MockProcess(stdout: 'master'), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + 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, plugin2.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); expect( output, containsAllInOrder([ - contains('--packages-for-branch: running on all packages'), + 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 { @@ -747,18 +894,19 @@ packages/b_package/lib/src/foo.dart group('sharding', () { test('distributes evenly when evenly divisible', () async { - final List> expectedShards = >[ - [ + 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), @@ -766,7 +914,7 @@ packages/b_package/lib/src/foo.dart ]; for (int i = 0; i < expectedShards.length; ++i) { - final SamplePluginCommand localCommand = SamplePluginCommand( + final SamplePackageCommand localCommand = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, @@ -784,32 +932,33 @@ packages/b_package/lib/src/foo.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory packageDir) => packageDir.path) + .map((RepositoryPackage package) => package.path) .toList())); } }); test('distributes as evenly as possible when not evenly divisible', () async { - final List> expectedShards = >[ - [ + 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 SamplePluginCommand localCommand = SamplePluginCommand( + final SamplePackageCommand localCommand = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, @@ -827,7 +976,7 @@ packages/b_package/lib/src/foo.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory packageDir) => packageDir.path) + .map((RepositoryPackage package) => package.path) .toList())); } }); @@ -841,18 +990,19 @@ packages/b_package/lib/src/foo.dart // 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 = >[ - [ + final List> expectedShards = + >[ + [ createFakePackage('package1', packagesDir), createFakePackage('package2', packagesDir), createFakePackage('package3', packagesDir), ], - [ + [ createFakePackage('package4', packagesDir), createFakePackage('package5', packagesDir), createFakePackage('package6', packagesDir), ], - [ + [ createFakePackage('package7', packagesDir), ], ]; @@ -861,7 +1011,7 @@ packages/b_package/lib/src/foo.dart createFakePackage('package9', packagesDir); for (int i = 0; i < expectedShards.length; ++i) { - final SamplePluginCommand localCommand = SamplePluginCommand( + final SamplePackageCommand localCommand = SamplePackageCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, @@ -880,24 +1030,27 @@ packages/b_package/lib/src/foo.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory packageDir) => packageDir.path) + .map((RepositoryPackage package) => package.path) .toList())); } }); }); } -class SamplePluginCommand extends PluginCommand { - SamplePluginCommand( +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'; @@ -906,7 +1059,10 @@ class SamplePluginCommand extends PluginCommand { @override Future run() async { - await for (final PackageEnumerationEntry entry in getTargetPackages()) { + 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/plugin_command_test.mocks.dart b/script/tool/test/common/package_command_test.mocks.dart similarity index 100% rename from script/tool/test/common/plugin_command_test.mocks.dart rename to script/tool/test/common/package_command_test.mocks.dart diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 6e46a3330cc8..c858df0022cc 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -11,7 +11,6 @@ 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:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; @@ -19,7 +18,7 @@ import 'package:test/test.dart'; import '../mocks.dart'; import '../util.dart'; -import 'plugin_command_test.mocks.dart'; +import 'package_command_test.mocks.dart'; // Constants for colorized output start and end. const String _startElapsedTimeColor = '\x1B[90m'; @@ -31,6 +30,21 @@ 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. @@ -40,6 +54,30 @@ 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; @@ -60,7 +98,7 @@ void main() { TestPackageLoopingCommand createTestCommand({ String gitDiffResponse = '', bool hasLongOutput = true, - bool includeSubpackages = false, + PackageLoopingType packageLoopingType = PackageLoopingType.topLevelOnly, bool failsDuringInit = false, bool warnsDuringInit = false, bool warnsDuringCleanup = false, @@ -84,7 +122,7 @@ void main() { packagesDir, platform: mockPlatform, hasLongOutput: hasLongOutput, - includeSubpackages: includeSubpackages, + packageLoopingType: packageLoopingType, failsDuringInit: failsDuringInit, warnsDuringInit: warnsDuringInit, warnsDuringCleanup: warnsDuringCleanup, @@ -122,10 +160,10 @@ void main() { test('does not stop looping on error', () async { createFakePackage('package_a', packagesDir); - final Directory failingPackage = + final RepositoryPackage failingPackage = createFakePlugin('package_b', packagesDir); createFakePackage('package_c', packagesDir); - failingPackage.childFile(_errorFile).createSync(); + _addResultFile(failingPackage, _ResultFileType.errors); final TestPackageLoopingCommand command = createTestCommand(hasLongOutput: false); @@ -147,10 +185,10 @@ void main() { test('does not stop looping on exceptions', () async { createFakePackage('package_a', packagesDir); - final Directory failingPackage = + final RepositoryPackage failingPackage = createFakePlugin('package_b', packagesDir); createFakePackage('package_c', packagesDir); - failingPackage.childFile(_throwFile).createSync(); + _addResultFile(failingPackage, _ResultFileType.throws); final TestPackageLoopingCommand command = createTestCommand(hasLongOutput: false); @@ -173,8 +211,10 @@ void main() { group('package iteration', () { test('includes plugins and packages', () async { - final Directory plugin = createFakePlugin('a_plugin', packagesDir); - final Directory package = createFakePackage('a_package', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('a_plugin', packagesDir); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); final TestPackageLoopingCommand command = createTestCommand(); await runCommand(command); @@ -184,8 +224,9 @@ void main() { }); test('includes third_party/packages', () async { - final Directory package1 = createFakePackage('a_package', packagesDir); - final Directory package2 = + final RepositoryPackage package1 = + createFakePackage('a_package', packagesDir); + final RepositoryPackage package2 = createFakePackage('another_package', thirdPartyPackagesDir); final TestPackageLoopingCommand command = createTestCommand(); @@ -195,46 +236,169 @@ void main() { unorderedEquals([package1.path, package2.path])); }); - test('includes subpackages when requested', () async { - final Directory plugin = createFakePlugin('a_plugin', packagesDir, + test('includes all subpackages when requested', () async { + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, examples: ['example1', 'example2']); - final Directory package = createFakePackage('a_package', packagesDir); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + final RepositoryPackage subPackage = createFakePackage( + 'sub_package', package.directory, + examples: []); - final TestPackageLoopingCommand command = - createTestCommand(includeSubpackages: true); + 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, - plugin.childDirectory('example').childDirectory('example1').path, - plugin.childDirectory('example').childDirectory('example2').path, + getExampleDir(plugin).childDirectory('example1').path, + getExampleDir(plugin).childDirectory('example2').path, package.path, - package.childDirectory('example').path, + getExampleDir(package).path, ])); + expect(command.checkedPackages, isNot(contains(subPackage.path))); }); test('excludes subpackages when main package is excluded', () async { - final Directory excluded = createFakePlugin('a_plugin', packagesDir, + final RepositoryPackage excluded = createFakePlugin( + 'a_plugin', packagesDir, examples: ['example1', 'example2']); - final Directory included = createFakePackage('a_package', packagesDir); + final RepositoryPackage included = + createFakePackage('a_package', packagesDir); + final RepositoryPackage subpackage = + createFakePackage('sub_package', excluded.directory); - final TestPackageLoopingCommand command = - createTestCommand(includeSubpackages: true); + 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, - included.childDirectory('example').path, + getExampleDir(included).path, ])); expect(command.checkedPackages, isNot(contains(excluded.path))); - expect(command.checkedPackages, - isNot(contains(excluded.childDirectory('example1').path))); - expect(command.checkedPackages, - isNot(contains(excluded.childDirectory('example2').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', + ])); }); }); @@ -243,8 +407,7 @@ void main() { createFakePlugin('package_a', packagesDir); createFakePackage('package_b', packagesDir); - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: true); + final TestPackageLoopingCommand command = createTestCommand(); final List output = await runCommand(command); const String separator = @@ -277,8 +440,7 @@ void main() { createFakePlugin('package_a', packagesDir); createFakePackage('package_b', packagesDir); - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: true); + final TestPackageLoopingCommand command = createTestCommand(); final List output = await runCommand(command, arguments: ['--log-timing']); @@ -332,13 +494,13 @@ void main() { test('shows failure summaries when something fails without extra details', () async { createFakePackage('package_a', packagesDir); - final Directory failingPackage1 = + final RepositoryPackage failingPackage1 = createFakePlugin('package_b', packagesDir); createFakePackage('package_c', packagesDir); - final Directory failingPackage2 = + final RepositoryPackage failingPackage2 = createFakePlugin('package_d', packagesDir); - failingPackage1.childFile(_errorFile).createSync(); - failingPackage2.childFile(_errorFile).createSync(); + _addResultFile(failingPackage1, _ResultFileType.errors); + _addResultFile(failingPackage2, _ResultFileType.errors); final TestPackageLoopingCommand command = createTestCommand(hasLongOutput: false); @@ -362,13 +524,13 @@ void main() { test('uses custom summary header and footer if provided', () async { createFakePackage('package_a', packagesDir); - final Directory failingPackage1 = + final RepositoryPackage failingPackage1 = createFakePlugin('package_b', packagesDir); createFakePackage('package_c', packagesDir); - final Directory failingPackage2 = + final RepositoryPackage failingPackage2 = createFakePlugin('package_d', packagesDir); - failingPackage1.childFile(_errorFile).createSync(); - failingPackage2.childFile(_errorFile).createSync(); + _addResultFile(failingPackage1, _ResultFileType.errors); + _addResultFile(failingPackage2, _ResultFileType.errors); final TestPackageLoopingCommand command = createTestCommand( hasLongOutput: false, @@ -395,17 +557,15 @@ void main() { test('shows failure summaries when something fails with extra details', () async { createFakePackage('package_a', packagesDir); - final Directory failingPackage1 = + final RepositoryPackage failingPackage1 = createFakePlugin('package_b', packagesDir); createFakePackage('package_c', packagesDir); - final Directory failingPackage2 = + final RepositoryPackage failingPackage2 = createFakePlugin('package_d', packagesDir); - final File errorFile1 = failingPackage1.childFile(_errorFile); - errorFile1.createSync(); - errorFile1.writeAsStringSync('just one detail'); - final File errorFile2 = failingPackage2.childFile(_errorFile); - errorFile2.createSync(); - errorFile2.writeAsStringSync('first detail\nsecond detail'); + _addResultFile(failingPackage1, _ResultFileType.errors, + contents: 'just one detail'); + _addResultFile(failingPackage2, _ResultFileType.errors, + contents: 'first detail\nsecond detail'); final TestPackageLoopingCommand command = createTestCommand(hasLongOutput: false); @@ -432,7 +592,7 @@ void main() { createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: true, captureOutput: true); + createTestCommand(captureOutput: true); final List output = await runCommand(command); expect(output, isEmpty); @@ -451,8 +611,10 @@ void main() { test('logs skips', () async { createFakePackage('package_a', packagesDir); - final Directory skipPackage = createFakePackage('package_b', packagesDir); - skipPackage.childFile(_skipFile).writeAsStringSync('For a reason'); + final RepositoryPackage skipPackage = + createFakePackage('package_b', packagesDir); + _addResultFile(skipPackage, _ResultFileType.skips, + contents: 'For a reason'); final TestPackageLoopingCommand command = createTestCommand(hasLongOutput: false); @@ -485,10 +647,10 @@ void main() { }); test('logs warnings', () async { - final Directory warnPackage = createFakePackage('package_a', packagesDir); - warnPackage - .childFile(_warningFile) - .writeAsStringSync('Warning 1\nWarning 2'); + final RepositoryPackage warnPackage = + createFakePackage('package_a', packagesDir); + _addResultFile(warnPackage, _ResultFileType.warns, + contents: 'Warning 1\nWarning 2'); createFakePackage('package_b', packagesDir); final TestPackageLoopingCommand command = @@ -507,10 +669,10 @@ void main() { test('logs unhandled exceptions as errors', () async { createFakePackage('package_a', packagesDir); - final Directory failingPackage = + final RepositoryPackage failingPackage = createFakePlugin('package_b', packagesDir); createFakePackage('package_c', packagesDir); - failingPackage.childFile(_throwFile).createSync(); + _addResultFile(failingPackage, _ResultFileType.throws); final TestPackageLoopingCommand command = createTestCommand(hasLongOutput: false); @@ -531,23 +693,30 @@ void main() { }); test('prints run summary on success', () async { - final Directory warnPackage1 = + final RepositoryPackage warnPackage1 = createFakePackage('package_a', packagesDir); - warnPackage1 - .childFile(_warningFile) - .writeAsStringSync('Warning 1\nWarning 2'); + _addResultFile(warnPackage1, _ResultFileType.warns, + contents: 'Warning 1\nWarning 2'); + createFakePackage('package_b', packagesDir); - final Directory skipPackage = createFakePackage('package_c', packagesDir); - skipPackage.childFile(_skipFile).writeAsStringSync('For a reason'); - final Directory skipAndWarnPackage = + + final RepositoryPackage skipPackage = + createFakePackage('package_c', packagesDir); + _addResultFile(skipPackage, _ResultFileType.skips, + contents: 'For a reason'); + + final RepositoryPackage skipAndWarnPackage = createFakePackage('package_d', packagesDir); - skipAndWarnPackage.childFile(_warningFile).writeAsStringSync('Warning'); - skipAndWarnPackage.childFile(_skipFile).writeAsStringSync('See warning'); - final Directory warnPackage2 = + _addResultFile(skipAndWarnPackage, _ResultFileType.warns, + contents: 'Warning'); + _addResultFile(skipAndWarnPackage, _ResultFileType.skips, + contents: 'See warning'); + + final RepositoryPackage warnPackage2 = createFakePackage('package_e', packagesDir); - warnPackage2 - .childFile(_warningFile) - .writeAsStringSync('Warning 1\nWarning 2'); + _addResultFile(warnPackage2, _ResultFileType.warns, + contents: 'Warning 1\nWarning 2'); + createFakePackage('package_f', packagesDir); final TestPackageLoopingCommand command = @@ -587,27 +756,33 @@ void main() { }); test('prints long-form run summary for long-output commands', () async { - final Directory warnPackage1 = + final RepositoryPackage warnPackage1 = createFakePackage('package_a', packagesDir); - warnPackage1 - .childFile(_warningFile) - .writeAsStringSync('Warning 1\nWarning 2'); + _addResultFile(warnPackage1, _ResultFileType.warns, + contents: 'Warning 1\nWarning 2'); + createFakePackage('package_b', packagesDir); - final Directory skipPackage = createFakePackage('package_c', packagesDir); - skipPackage.childFile(_skipFile).writeAsStringSync('For a reason'); - final Directory skipAndWarnPackage = + + final RepositoryPackage skipPackage = + createFakePackage('package_c', packagesDir); + _addResultFile(skipPackage, _ResultFileType.skips, + contents: 'For a reason'); + + final RepositoryPackage skipAndWarnPackage = createFakePackage('package_d', packagesDir); - skipAndWarnPackage.childFile(_warningFile).writeAsStringSync('Warning'); - skipAndWarnPackage.childFile(_skipFile).writeAsStringSync('See warning'); - final Directory warnPackage2 = + _addResultFile(skipAndWarnPackage, _ResultFileType.warns, + contents: 'Warning'); + _addResultFile(skipAndWarnPackage, _ResultFileType.skips, + contents: 'See warning'); + + final RepositoryPackage warnPackage2 = createFakePackage('package_e', packagesDir); - warnPackage2 - .childFile(_warningFile) - .writeAsStringSync('Warning 1\nWarning 2'); + _addResultFile(warnPackage2, _ResultFileType.warns, + contents: 'Warning 1\nWarning 2'); + createFakePackage('package_f', packagesDir); - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: true); + final TestPackageLoopingCommand command = createTestCommand(); final List output = await runCommand(command); expect( @@ -632,8 +807,7 @@ void main() { test('prints exclusions as skips in long-form run summary', () async { createFakePackage('package_a', packagesDir); - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: true); + final TestPackageLoopingCommand command = createTestCommand(); final List output = await runCommand(command, arguments: ['--exclude=package_a']); @@ -679,7 +853,7 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { Directory packagesDir, { required Platform platform, this.hasLongOutput = true, - this.includeSubpackages = false, + this.packageLoopingType = PackageLoopingType.topLevelOnly, this.customFailureListHeader, this.customFailureListFooter, this.failsDuringInit = false, @@ -705,7 +879,7 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { bool hasLongOutput; @override - bool includeSubpackages; + PackageLoopingType packageLoopingType; @override String get failureListHeader => diff --git a/script/tool/test/common/package_state_utils_test.dart b/script/tool/test/common/package_state_utils_test.dart new file mode 100644 index 000000000000..c9ae5ba4c742 --- /dev/null +++ b/script/tool/test/common/package_state_utils_test.dart @@ -0,0 +1,341 @@ +// 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', + ]; + + 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 index cedd40acb7d6..415b1db8932a 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -6,7 +6,6 @@ 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/common/repository_package.dart'; import 'package:test/test.dart'; import '../util.dart'; @@ -22,8 +21,7 @@ void main() { group('pluginSupportsPlatform', () { test('no platforms', () async { - final RepositoryPackage plugin = - RepositoryPackage(createFakePlugin('plugin', packagesDir)); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); expect(pluginSupportsPlatform(platformAndroid, plugin), isFalse); expect(pluginSupportsPlatform(platformIOS, plugin), isFalse); @@ -34,8 +32,7 @@ void main() { }); test('all platforms', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), platformIOS: const PlatformDetails(PlatformSupport.inline), @@ -43,7 +40,7 @@ void main() { 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); @@ -54,13 +51,12 @@ void main() { }); test('some platforms', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, + 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); @@ -71,8 +67,7 @@ void main() { }); test('inline plugins are only detected as inline', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), platformIOS: const PlatformDetails(PlatformSupport.inline), @@ -80,7 +75,7 @@ void main() { platformMacOS: const PlatformDetails(PlatformSupport.inline), platformWeb: const PlatformDetails(PlatformSupport.inline), platformWindows: const PlatformDetails(PlatformSupport.inline), - })); + }); expect( pluginSupportsPlatform(platformAndroid, plugin, @@ -133,8 +128,7 @@ void main() { }); test('federated plugins are only detected as federated', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.federated), platformIOS: const PlatformDetails(PlatformSupport.federated), @@ -142,7 +136,7 @@ void main() { platformMacOS: const PlatformDetails(PlatformSupport.federated), platformWeb: const PlatformDetails(PlatformSupport.federated), platformWindows: const PlatformDetails(PlatformSupport.federated), - })); + }); expect( pluginSupportsPlatform(platformAndroid, plugin, @@ -193,102 +187,23 @@ void main() { requiredMode: PlatformSupport.inline), isFalse); }); - - test('windows without variants is only win32', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }, - )); - - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWin32), - isTrue); - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWinUwp), - isFalse); - }); - - test('windows with both variants matches win32 and winuwp', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformWindows: const PlatformDetails( - PlatformSupport.federated, - variants: [platformVariantWin32, platformVariantWinUwp], - ), - })); - - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWin32), - isTrue); - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWinUwp), - isTrue); - }); - - test('win32 plugin is only win32', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformWindows: const PlatformDetails( - PlatformSupport.federated, - variants: [platformVariantWin32], - ), - })); - - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWin32), - isTrue); - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWinUwp), - isFalse); - }); - - test('winup plugin is only winuwp', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.federated, - variants: [platformVariantWinUwp]), - }, - )); - - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWin32), - isFalse); - expect( - pluginSupportsPlatform(platformWindows, plugin, - variant: platformVariantWinUwp), - isTrue); - }); }); group('pluginHasNativeCodeForPlatform', () { test('returns false for web', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 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 = RepositoryPackage(createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -296,7 +211,7 @@ void main() { platformMacOS: const PlatformDetails(PlatformSupport.inline), platformWindows: const PlatformDetails(PlatformSupport.inline), }, - )); + ); expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isTrue); expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isTrue); @@ -304,18 +219,15 @@ void main() { }); test('returns true for a native+Dart plugin', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline, - hasNativeCode: true, hasDartCode: true), - platformMacOS: const PlatformDetails(PlatformSupport.inline, - hasNativeCode: true, hasDartCode: true), - platformWindows: const PlatformDetails(PlatformSupport.inline, - hasNativeCode: true, hasDartCode: true), + 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); @@ -323,7 +235,7 @@ void main() { }); test('returns false for a Dart-only plugin', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -334,7 +246,7 @@ void main() { platformWindows: const PlatformDetails(PlatformSupport.inline, hasNativeCode: false, hasDartCode: true), }, - )); + ); expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isFalse); expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isFalse); diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart index 29e3b5832127..db519c008233 100644 --- a/script/tool/test/common/repository_package_test.dart +++ b/script/tool/test/common/repository_package_test.dart @@ -4,8 +4,6 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/test.dart'; import '../util.dart'; @@ -97,82 +95,126 @@ void main() { }); group('getExamples', () { - test('handles a single example', () async { - final Directory plugin = createFakePlugin('a_plugin', packagesDir); + test('handles a single Flutter example', () async { + final RepositoryPackage plugin = + createFakePlugin('a_plugin', packagesDir); - final List examples = - RepositoryPackage(plugin).getExamples().toList(); + final List examples = plugin.getExamples().toList(); expect(examples.length, 1); - expect(examples[0].path, plugin.childDirectory('example').path); + expect(examples[0].path, getExampleDir(plugin).path); }); - test('handles multiple examples', () async { - final Directory plugin = createFakePlugin('a_plugin', packagesDir, + test('handles multiple Flutter examples', () async { + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, examples: ['example1', 'example2']); - final List examples = - RepositoryPackage(plugin).getExamples().toList(); + final List examples = plugin.getExamples().toList(); expect(examples.length, 2); expect(examples[0].path, - plugin.childDirectory('example').childDirectory('example1').path); + getExampleDir(plugin).childDirectory('example1').path); expect(examples[1].path, - plugin.childDirectory('example').childDirectory('example2').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 Directory plugin = createFakePlugin('a_plugin', packagesDir); - expect(RepositoryPackage(plugin).isFederated, false); - expect(RepositoryPackage(plugin).isPlatformInterface, false); - expect(RepositoryPackage(plugin).isFederated, false); + 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 Directory plugin = + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); - expect(RepositoryPackage(plugin).isFederated, true); - expect(RepositoryPackage(plugin).isPlatformInterface, false); - expect(RepositoryPackage(plugin).isPlatformImplementation, false); + expect(plugin.isFederated, true); + expect(plugin.isAppFacing, true); + expect(plugin.isPlatformInterface, false); + expect(plugin.isPlatformImplementation, false); }); test('handle platform interface packages', () { - final Directory plugin = createFakePlugin('a_plugin_platform_interface', + final RepositoryPackage plugin = createFakePlugin( + 'a_plugin_platform_interface', packagesDir.childDirectory('a_plugin')); - expect(RepositoryPackage(plugin).isFederated, true); - expect(RepositoryPackage(plugin).isPlatformInterface, true); - expect(RepositoryPackage(plugin).isPlatformImplementation, false); + 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 Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin_foo', packagesDir.childDirectory('a_plugin')); - expect(RepositoryPackage(plugin).isFederated, true); - expect(RepositoryPackage(plugin).isPlatformInterface, false); - expect(RepositoryPackage(plugin).isPlatformImplementation, true); + expect(plugin.isFederated, true); + expect(plugin.isAppFacing, false); + expect(plugin.isPlatformInterface, false); + expect(plugin.isPlatformImplementation, true); }); }); group('pubspec', () { test('file', () async { - final Directory plugin = createFakePlugin('a_plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('a_plugin', packagesDir); - final File pubspecFile = RepositoryPackage(plugin).pubspecFile; + final File pubspecFile = plugin.pubspecFile; - expect(pubspecFile.path, plugin.childFile('pubspec.yaml').path); + expect(pubspecFile.path, plugin.directory.childFile('pubspec.yaml').path); }); test('parsing', () async { - final Directory plugin = createFakePlugin('a_plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, examples: ['example1', 'example2']); - final Pubspec pubspec = RepositoryPackage(plugin).parsePubspec(); + 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/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 0066cc53f61a..cb2347fe9cc8 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -2,12 +2,17 @@ // 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_plugins_app_command.dart'; +import 'package:platform/platform.dart'; import 'package:test/test.dart'; +import 'mocks.dart'; import 'util.dart'; void main() { @@ -17,6 +22,7 @@ void main() { 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 @@ -25,9 +31,11 @@ void main() { fileSystem = const LocalFileSystem(); testRoot = fileSystem.systemTempDirectory.createTempSync(); packagesDir = testRoot.childDirectory('packages'); + processRunner = RecordingProcessRunner(); command = CreateAllPluginsAppCommand( packagesDir, + processRunner: processRunner, pluginsRoot: testRoot, ); runner = CommandRunner( @@ -45,8 +53,7 @@ void main() { createFakePlugin('pluginc', packagesDir); await runCapturingPrint(runner, ['all-plugins-app']); - final List pubspec = - command.appDirectory.childFile('pubspec.yaml').readAsLinesSync(); + final List pubspec = command.app.pubspecFile.readAsLinesSync(); expect( pubspec, @@ -63,8 +70,7 @@ void main() { createFakePlugin('pluginc', packagesDir); await runCapturingPrint(runner, ['all-plugins-app']); - final List pubspec = - command.appDirectory.childFile('pubspec.yaml').readAsLinesSync(); + final List pubspec = command.app.pubspecFile.readAsLinesSync(); expect( pubspec, @@ -76,16 +82,117 @@ void main() { ])); }); - test('pubspec is compatible with null-safe app code', () async { + 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, ['all-plugins-app']); - final String pubspec = - command.appDirectory.childFile('pubspec.yaml').readAsStringSync(); + final Pubspec generatedPubspec = command.app.parsePubspec(); - expect(pubspec, contains(RegExp('sdk:\\s*(?:["\']>=|[^])2\\.12\\.'))); + const String dartSdkKey = 'sdk'; + expect(generatedPubspec.environment?[dartSdkKey], + baselinePubspec.environment?[dartSdkKey]); }); + test('macOS deployment target is modified in Podfile', () async { + createFakePlugin('plugina', packagesDir); + + final File podfileFile = command.packagesDir.parent + .childDirectory('all_plugins') + .childDirectory('macos') + .childFile('Podfile'); + podfileFile.createSync(recursive: true); + podfileFile.writeAsStringSync(""" +platform :osx, '10.11' +# some other line +"""); + + await runCapturingPrint(runner, ['all-plugins-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); + + test('macOS deployment target is modified in pbxproj', () async { + createFakePlugin('plugina', packagesDir); + + await runCapturingPrint(runner, ['all-plugins-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, ['all-plugins-app']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(const LocalPlatform()), + const ['pub', 'get'], + testRoot.childDirectory('all_plugins').path), + ])); + }, + // See comment about Windows in create_all_plugins_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, ['all-plugins-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_plugins_app_command.dart + skip: io.Platform.isWindows); + test('handles --output-dir', () async { createFakePlugin('plugina', packagesDir); @@ -94,8 +201,8 @@ void main() { await runCapturingPrint(runner, ['all-plugins-app', '--output-dir=${customOutputDir.path}']); - expect(command.appDirectory.path, - customOutputDir.childDirectory('all_plugins').path); + expect( + command.app.path, customOutputDir.childDirectory('all_plugins').path); }); test('logs exclusions', () async { diff --git a/script/tool/test/custom_test_command_test.dart b/script/tool/test/custom_test_command_test.dart new file mode 100644 index 000000000000..8b0c021b1255 --- /dev/null +++ b/script/tool/test/custom_test_command_test.dart @@ -0,0 +1,328 @@ +// 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 new file mode 100644 index 000000000000..39dd8f4fcb92 --- /dev/null +++ b/script/tool/test/dependabot_check_command_test.dart @@ -0,0 +1,141 @@ +// 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 index 9372c571b6f7..0b6082098ae8 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -128,6 +128,7 @@ void main() { extraFiles: [ 'example/test_driver/integration_test.dart', 'example/integration_test/foo_test.dart', + 'example/ios/ios.m', ], platformSupport: { platformIOS: const PlatformDetails(PlatformSupport.inline), @@ -187,12 +188,14 @@ void main() { }); test('driving under folder "test_driver"', () async { - final Directory pluginDirectory = createFakePlugin( + 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), @@ -200,8 +203,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); setMockFlutterDevicesOutput(); final List output = @@ -243,6 +245,8 @@ void main() { packagesDir, extraFiles: [ 'example/test_driver/plugin_test.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -276,6 +280,8 @@ void main() { packagesDir, extraFiles: [ 'example/lib/main.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline), @@ -301,10 +307,51 @@ void main() { ); }); + 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 Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -312,6 +359,8 @@ void main() { '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), @@ -319,8 +368,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); setMockFlutterDevicesOutput(); final List output = @@ -392,20 +440,20 @@ void main() { }); test('driving on a Linux plugin', () async { - final Directory pluginDirectory = createFakePlugin( + 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 = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -464,7 +512,7 @@ void main() { }); test('driving on a macOS plugin', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -477,8 +525,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -536,20 +583,20 @@ void main() { }); test('driving a web plugin', () async { - final Directory pluginDirectory = createFakePlugin( + 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 = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -585,20 +632,20 @@ void main() { }); test('driving a web plugin with CHROME_EXECUTABLE', () async { - final Directory pluginDirectory = createFakePlugin( + 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 = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); mockPlatform.environment['CHROME_EXECUTABLE'] = '/path/to/chrome'; @@ -662,20 +709,20 @@ void main() { }); test('driving on a Windows plugin', () async { - final Directory pluginDirectory = createFakePlugin( + 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 = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -708,55 +755,21 @@ void main() { ])); }); - test('driving UWP is a no-op', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ], - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline, - variants: [platformVariantWinUwp]), - }, - ); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--winuwp', - ]); - - expect( - output, - containsAllInOrder([ - contains('Driving UWP applications is not yet supported'), - contains('Running for plugin'), - contains('SKIPPING: Drive does not yet support UWP'), - 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 an Android plugin', () async { - final Directory pluginDirectory = createFakePlugin( + 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 = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); setMockFlutterDevicesOutput(); final List output = await runCapturingPrint(runner, [ @@ -881,12 +894,14 @@ void main() { }); test('enable-experiment flag', () async { - final Directory pluginDirectory = createFakePlugin( + 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), @@ -894,8 +909,7 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); setMockFlutterDevicesOutput(); await runCapturingPrint(runner, [ @@ -961,6 +975,7 @@ void main() { extraFiles: [ 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', + 'example/web/index.html', ], platformSupport: { platformWeb: const PlatformDetails(PlatformSupport.inline), @@ -993,6 +1008,7 @@ void main() { packagesDir, extraFiles: [ 'example/test_driver/integration_test.dart', + 'example/web/index.html', ], platformSupport: { platformWeb: const PlatformDetails(PlatformSupport.inline), @@ -1022,13 +1038,14 @@ void main() { }); test('reports test failures', () async { - final Directory pluginDirectory = createFakePlugin( + 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), @@ -1063,8 +1080,7 @@ void main() { ]), ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); expect( processRunner.recordedCalls, orderedEquals([ @@ -1094,5 +1110,148 @@ void main() { 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 index 126aa8b41c83..6b6b1a514531 100644 --- a/script/tool/test/federation_safety_check_command_test.dart +++ b/script/tool/test/federation_safety_check_command_test.dart @@ -8,12 +8,11 @@ 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/repository_package.dart'; import 'package:flutter_plugin_tools/src/federation_safety_check_command.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; -import 'common/plugin_command_test.mocks.dart'; +import 'common/package_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; @@ -55,10 +54,10 @@ void main() { }); test('skips non-plugin packages', () async { - final Directory package = createFakePackage('foo', packagesDir); + final RepositoryPackage package = createFakePackage('foo', packagesDir); final String changedFileOutput = [ - package.childDirectory('lib').childFile('foo.dart'), + package.libDirectory.childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), @@ -78,10 +77,10 @@ void main() { }); test('skips unfederated plugins', () async { - final Directory package = createFakePlugin('foo', packagesDir); + final RepositoryPackage package = createFakePlugin('foo', packagesDir); final String changedFileOutput = [ - package.childDirectory('lib').childFile('foo.dart'), + package.libDirectory.childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), @@ -102,11 +101,11 @@ void main() { test('skips interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory platformInterface = + final RepositoryPackage platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ - platformInterface.childDirectory('lib').childFile('foo.dart'), + platformInterface.libDirectory.childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), @@ -127,15 +126,15 @@ void main() { test('allows changes to just an interface package', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory platformInterface = + final RepositoryPackage platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); createFakePlugin('foo', pluginGroupDir); createFakePlugin('foo_ios', pluginGroupDir); createFakePlugin('foo_android', pluginGroupDir); final String changedFileOutput = [ - platformInterface.childDirectory('lib').childFile('foo.dart'), - platformInterface.childFile('pubspec.yaml'), + platformInterface.libDirectory.childFile('foo.dart'), + platformInterface.pubspecFile, ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), @@ -168,14 +167,14 @@ void main() { test('allows changes to multiple non-interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory appFacing = createFakePlugin('foo', pluginGroupDir); - final Directory implementation = + final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); + final RepositoryPackage implementation = createFakePlugin('foo_bar', pluginGroupDir); createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ - appFacing.childFile('foo.dart'), - implementation.childFile('foo.dart'), + appFacing.libDirectory.childFile('foo.dart'), + implementation.libDirectory.childFile('foo.dart'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), @@ -199,17 +198,17 @@ void main() { 'fails on changes to interface and non-interface packages in the same plugin', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory appFacing = createFakePlugin('foo', pluginGroupDir); - final Directory implementation = + final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); + final RepositoryPackage implementation = createFakePlugin('foo_bar', pluginGroupDir); - final Directory platformInterface = + final RepositoryPackage platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ - appFacing.childFile('foo.dart'), - implementation.childFile('foo.dart'), - platformInterface.childFile('pubspec.yaml'), - platformInterface.childDirectory('lib').childFile('foo.dart'), + 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), @@ -244,17 +243,17 @@ void main() { test('ignores test-only changes to interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory appFacing = createFakePlugin('foo', pluginGroupDir); - final Directory implementation = + final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); + final RepositoryPackage implementation = createFakePlugin('foo_bar', pluginGroupDir); - final Directory platformInterface = + final RepositoryPackage platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ - appFacing.childFile('foo.dart'), - implementation.childFile('foo.dart'), - platformInterface.childFile('pubspec.yaml'), - platformInterface.childDirectory('test').childFile('foo.dart'), + 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), @@ -276,27 +275,24 @@ void main() { test('ignores unpublished changes to interface packages', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory appFacing = createFakePlugin('foo', pluginGroupDir); - final Directory implementation = + final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); + final RepositoryPackage implementation = createFakePlugin('foo_bar', pluginGroupDir); - final Directory platformInterface = + final RepositoryPackage platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); final String changedFileOutput = [ - appFacing.childFile('foo.dart'), - implementation.childFile('foo.dart'), - platformInterface.childFile('pubspec.yaml'), - platformInterface.childDirectory('lib').childFile('foo.dart'), + 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: RepositoryPackage(platformInterface) - .pubspecFile - .readAsStringSync()), + MockProcess(stdout: platformInterface.pubspecFile.readAsStringSync()), ]; final List output = @@ -315,22 +311,22 @@ void main() { test('allows things that look like mass changes, with warning', () async { final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final Directory appFacing = createFakePlugin('foo', pluginGroupDir); - final Directory implementation = + final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); + final RepositoryPackage implementation = createFakePlugin('foo_bar', pluginGroupDir); - final Directory platformInterface = + final RepositoryPackage platformInterface = createFakePlugin('foo_platform_interface', pluginGroupDir); - final Directory otherPlugin1 = createFakePlugin('bar', packagesDir); - final Directory otherPlugin2 = createFakePlugin('baz', packagesDir); + final RepositoryPackage otherPlugin1 = createFakePlugin('bar', packagesDir); + final RepositoryPackage otherPlugin2 = createFakePlugin('baz', packagesDir); final String changedFileOutput = [ - appFacing.childFile('foo.dart'), - implementation.childFile('foo.dart'), - platformInterface.childFile('pubspec.yaml'), - platformInterface.childDirectory('lib').childFile('foo.dart'), - otherPlugin1.childFile('bar.dart'), - otherPlugin2.childFile('baz.dart'), + 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), @@ -355,11 +351,11 @@ void main() { test('handles top-level files that match federated package heuristics', () async { - final Directory plugin = createFakePlugin('foo', packagesDir); + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir); final String changedFileOutput = [ // This should be picked up as a change to 'foo', and not crash. - plugin.childFile('foo_bar.baz'), + plugin.directory.childFile('foo_bar.baz'), ].map((File file) => file.path).join('\n'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: changedFileOutput), diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 1dfd8ba66b58..68ea62b2334f 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -17,7 +17,7 @@ import 'mocks.dart'; import 'util.dart'; void main() { - group('$FirebaseTestLabCommand', () { + group('FirebaseTestLabCommand', () { FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; @@ -40,9 +40,10 @@ void main() { runner.addCommand(command); }); - void _writeJavaTestFile(Directory pluginDir, String relativeFilePath, + void writeJavaTestFile(RepositoryPackage plugin, String relativeFilePath, {String runnerClass = 'FlutterTestRunner'}) { - childFileWithSubcomponents(pluginDir, p.posix.split(relativeFilePath)) + childFileWithSubcomponents( + plugin.directory, p.posix.split(relativeFilePath)) .writeAsStringSync(''' @DartIntegrationTest @RunWith($runnerClass.class) @@ -60,13 +61,13 @@ public class MainActivityTest { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); Error? commandError; final List output = await runCapturingPrint( @@ -90,13 +91,13 @@ public class MainActivityTest { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, ['firebase-test-lab']); @@ -112,22 +113,22 @@ public class MainActivityTest { test('only runs gcloud configuration once', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory plugin1Dir = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, extraFiles: [ 'test/plugin_test.dart', 'example/integration_test/foo_test.dart', 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(plugin1Dir, javaTestFileRelativePath); - final Directory plugin2Dir = + 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(plugin2Dir, javaTestFileRelativePath); + writeJavaTestFile(plugin2, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', @@ -173,7 +174,7 @@ public class MainActivityTest { '/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_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' + '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( @@ -187,7 +188,7 @@ public class MainActivityTest { '/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_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' + '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'), ]), @@ -197,7 +198,7 @@ public class MainActivityTest { test('runs integration tests', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'test/plugin_test.dart', 'example/integration_test/bar_test.dart', @@ -206,7 +207,7 @@ public class MainActivityTest { 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', @@ -254,7 +255,7 @@ public class MainActivityTest { '/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_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30 --device model=seoul,version=26' + '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( @@ -264,24 +265,89 @@ public class MainActivityTest { '/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_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=redfin,version=30 --device model=seoul,version=26' + '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 Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); processRunner.mockProcessesForExecutable['gcloud'] = [ MockProcess(), // auth @@ -320,14 +386,14 @@ public class MainActivityTest { () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); processRunner.mockProcessesForExecutable['gcloud'] = [ MockProcess(), // auth @@ -389,12 +455,12 @@ public class MainActivityTest { test('fails for packages with no integration test files', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); Error? commandError; final List output = await runCapturingPrint( @@ -425,7 +491,7 @@ public class MainActivityTest { test('fails for packages with no integration_test runner', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'test/plugin_test.dart', 'example/integration_test/bar_test.dart', @@ -435,7 +501,7 @@ public class MainActivityTest { javaTestFileRelativePath, ]); // Use the wrong @RunWith annotation. - _writeJavaTestFile(pluginDir, javaTestFileRelativePath, + writeJavaTestFile(plugin, javaTestFileRelativePath, runnerClass: 'AndroidJUnit4.class'); Error? commandError; @@ -479,7 +545,7 @@ public class MainActivityTest { output, containsAllInOrder([ contains('Running for package'), - contains('package/example does not support Android'), + contains('No examples support Android'), ]), ); expect(output, @@ -494,12 +560,12 @@ public class MainActivityTest { test('builds if gradlew is missing', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', @@ -547,7 +613,7 @@ public class MainActivityTest { '/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_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30' + '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'), ]), @@ -557,12 +623,12 @@ public class MainActivityTest { test('fails if building to generate gradlew fails', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); processRunner.mockProcessesForExecutable['flutter'] = [ MockProcess(exitCode: 1) // flutter build @@ -592,16 +658,17 @@ public class MainActivityTest { test('fails if assembleAndroidTest fails', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ @@ -632,16 +699,17 @@ public class MainActivityTest { test('fails if assembleDebug fails', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ @@ -676,13 +744,13 @@ public class MainActivityTest { test('experimental flag', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', javaTestFileRelativePath, ]); - _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + writeJavaTestFile(plugin, javaTestFileRelativePath); await runCapturingPrint(runner, [ 'firebase-test-lab', @@ -717,7 +785,7 @@ public class MainActivityTest { '/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_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=redfin,version=30' + '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 new file mode 100644 index 000000000000..16061d2206cd --- /dev/null +++ b/script/tool/test/fix_command_test.dart @@ -0,0 +1,78 @@ +// 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 index 2890c528e4c1..9a865053a2b6 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -49,26 +49,26 @@ void main() { /// Returns a modified version of a list of [relativePaths] that are relative /// to [package] to instead be relative to [packagesDir]. - List _getPackagesDirRelativePaths( - Directory packageDir, List relativePaths) { + List getPackagesDirRelativePaths( + RepositoryPackage package, List relativePaths) { final p.Context path = analyzeCommand.path; final String relativeBase = - path.relative(packageDir.path, from: packagesDir.path); + 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] - /// with name [pluginName] such that each path will be 99 characters long - /// relative to [packagesDir]. + /// 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 pluginName, int count) { + List get99CharacterPathExtraFiles(String packageName, int count) { final int padding = 99 - - pluginName.length - - 1 - // the path separator after the plugin name + 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; @@ -86,7 +86,7 @@ void main() { 'lib/src/b.dart', 'lib/src/c.dart', ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, @@ -99,10 +99,7 @@ void main() { orderedEquals([ ProcessCall( getFlutterCommand(mockPlatform), - [ - 'format', - ..._getPackagesDirRelativePaths(pluginDir, files) - ], + ['format', ...getPackagesDirRelativePaths(plugin, files)], packagesDir.path), ])); }); @@ -114,7 +111,7 @@ void main() { 'lib/src/c.dart', ]; const String unformattedFile = 'lib/src/d.dart'; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: [ @@ -124,7 +121,8 @@ void main() { ); final p.Context posixContext = p.posix; - childFileWithSubcomponents(pluginDir, posixContext.split(unformattedFile)) + childFileWithSubcomponents( + plugin.directory, posixContext.split(unformattedFile)) .writeAsStringSync( '// copyright bla bla\n// This file is hand-formatted.\ncode...'); @@ -137,7 +135,7 @@ void main() { getFlutterCommand(mockPlatform), [ 'format', - ..._getPackagesDirRelativePaths(pluginDir, formattedFiles) + ...getPackagesDirRelativePaths(plugin, formattedFiles) ], packagesDir.path), ])); @@ -172,7 +170,7 @@ void main() { 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, @@ -190,7 +188,7 @@ void main() { '-jar', javaFormatPath, '--replace', - ..._getPackagesDirRelativePaths(pluginDir, files) + ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -217,7 +215,7 @@ void main() { output, containsAllInOrder([ contains( - 'Unable to run \'java\'. Make sure that it is in your path, or ' + 'Unable to run "java". Make sure that it is in your path, or ' 'provide a full path with --java.'), ])); }); @@ -252,7 +250,7 @@ void main() { 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, @@ -270,7 +268,7 @@ void main() { '-jar', javaFormatPath, '--replace', - ..._getPackagesDirRelativePaths(pluginDir, files) + ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -285,7 +283,7 @@ void main() { 'macos/Classes/Foo.mm', 'windows/foo_plugin.cpp', ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, @@ -302,7 +300,7 @@ void main() { [ '-i', '--style=file', - ..._getPackagesDirRelativePaths(pluginDir, files) + ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -329,8 +327,7 @@ void main() { expect( output, containsAllInOrder([ - contains( - 'Unable to run \'clang-format\'. Make sure that it is in your ' + contains('Unable to run "clang-format". Make sure that it is in your ' 'path, or provide a full path with --clang-format.'), ])); }); @@ -339,7 +336,7 @@ void main() { const List files = [ 'windows/foo_plugin.cpp', ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, @@ -358,7 +355,7 @@ void main() { [ '-i', '--style=file', - ..._getPackagesDirRelativePaths(pluginDir, files) + ...getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -403,7 +400,7 @@ void main() { const List javaFiles = [ 'android/src/main/java/io/flutter/plugins/a_plugin/a.java' ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: [ @@ -426,14 +423,14 @@ void main() { [ '-i', '--style=file', - ..._getPackagesDirRelativePaths(pluginDir, clangFiles) + ...getPackagesDirRelativePaths(plugin, clangFiles) ], packagesDir.path), ProcessCall( getFlutterCommand(mockPlatform), [ 'format', - ..._getPackagesDirRelativePaths(pluginDir, dartFiles) + ...getPackagesDirRelativePaths(plugin, dartFiles) ], packagesDir.path), ProcessCall( @@ -442,13 +439,13 @@ void main() { '-jar', javaFormatPath, '--replace', - ..._getPackagesDirRelativePaths(pluginDir, javaFiles) + ...getPackagesDirRelativePaths(plugin, javaFiles) ], packagesDir.path), ])); }); - test('fails if files are changed with --file-on-change', () async { + test('fails if files are changed with --fail-on-change', () async { const List files = [ 'linux/foo_plugin.cc', 'macos/Classes/Foo.h', @@ -541,7 +538,7 @@ void main() { // Make the file list one file longer than would fit in the batch. final List batch1 = - _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + get99CharacterPathExtraFiles(pluginName, batchSize + 1); final String extraFile = batch1.removeLast(); createFakePlugin( @@ -578,7 +575,7 @@ void main() { // Make the file list one file longer than would fit in a Windows batch. final List batch = - _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + get99CharacterPathExtraFiles(pluginName, batchSize + 1); createFakePlugin( pluginName, @@ -598,7 +595,7 @@ void main() { // Make the file list one file longer than would fit in the batch. final List batch1 = - _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + get99CharacterPathExtraFiles(pluginName, batchSize + 1); final String extraFile = batch1.removeLast(); createFakePlugin( diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index e97274afd09e..09841df74e70 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -7,24 +7,35 @@ 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', () { + 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'); @@ -37,7 +48,7 @@ void main() { /// [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( + void writeLicense( File file, { String comment = '// ', String prefix = '', @@ -123,10 +134,37 @@ void main() { } }); + 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); + writeLicense(checked); final File notChecked = root.childFile('not_checked.md'); notChecked.createSync(); @@ -145,7 +183,7 @@ void main() { test('passes correct license blocks on Windows', () async { final File checked = root.childFile('checked.cc'); checked.createSync(); - _writeLicense(checked, useCrlf: true); + writeLicense(checked, useCrlf: true); final List output = await runCapturingPrint(runner, ['license-check']); @@ -162,13 +200,13 @@ void main() { test('handles the comment styles for all supported languages', () async { final File fileA = root.childFile('file_a.cc'); fileA.createSync(); - _writeLicense(fileA, comment: '// '); + writeLicense(fileA); final File fileB = root.childFile('file_b.sh'); fileB.createSync(); - _writeLicense(fileB, comment: '# '); + writeLicense(fileB, comment: '# '); final File fileC = root.childFile('file_c.html'); fileC.createSync(); - _writeLicense(fileC, comment: '', prefix: ''); + writeLicense(fileC, comment: '', prefix: ''); final List output = await runCapturingPrint(runner, ['license-check']); @@ -187,10 +225,10 @@ void main() { test('fails if any checked files are missing license blocks', () async { final File goodA = root.childFile('good.cc'); goodA.createSync(); - _writeLicense(goodA); + writeLicense(goodA); final File goodB = root.childFile('good.h'); goodB.createSync(); - _writeLicense(goodB); + writeLicense(goodB); root.childFile('bad.cc').createSync(); root.childFile('bad.h').createSync(); @@ -217,10 +255,10 @@ void main() { test('fails if any checked files are missing just the copyright', () async { final File good = root.childFile('good.cc'); good.createSync(); - _writeLicense(good); + writeLicense(good); final File bad = root.childFile('bad.cc'); bad.createSync(); - _writeLicense(bad, copyright: ''); + writeLicense(bad, copyright: ''); Error? commandError; final List output = await runCapturingPrint( @@ -244,10 +282,10 @@ void main() { test('fails if any checked files are missing just the license', () async { final File good = root.childFile('good.cc'); good.createSync(); - _writeLicense(good); + writeLicense(good); final File bad = root.childFile('bad.cc'); bad.createSync(); - _writeLicense(bad, license: []); + writeLicense(bad, license: []); Error? commandError; final List output = await runCapturingPrint( @@ -272,7 +310,7 @@ void main() { () async { final File thirdPartyFile = root.childFile('third_party.cc'); thirdPartyFile.createSync(); - _writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); + writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); Error? commandError; final List output = await runCapturingPrint( @@ -301,7 +339,7 @@ void main() { .childDirectory('third_party') .childFile('file.cc'); thirdPartyFile.createSync(recursive: true); - _writeLicense(thirdPartyFile, + writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Workiva Inc.', license: [ 'Licensed under the Apache License, Version 2.0 (the "License");', @@ -328,7 +366,7 @@ void main() { .childDirectory('third_party') .childFile('first_party.cc'); firstPartyFileInThirdParty.createSync(recursive: true); - _writeLicense(firstPartyFileInThirdParty); + writeLicense(firstPartyFileInThirdParty); final List output = await runCapturingPrint(runner, ['license-check']); @@ -345,10 +383,10 @@ void main() { test('fails for licenses that the tool does not expect', () async { final File good = root.childFile('good.cc'); good.createSync(); - _writeLicense(good); + writeLicense(good); final File bad = root.childDirectory('third_party').childFile('bad.cc'); bad.createSync(recursive: true); - _writeLicense(bad, license: [ + 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', ]); @@ -376,10 +414,10 @@ void main() { () async { final File good = root.childFile('good.cc'); good.createSync(); - _writeLicense(good); + writeLicense(good); final File bad = root.childDirectory('third_party').childFile('bad.cc'); bad.createSync(recursive: true); - _writeLicense( + writeLicense( bad, copyright: 'Copyright 2017 Some New Authors.', license: [ @@ -509,6 +547,11 @@ void main() { }); } +class MockPlatformWithSeparator extends MockPlatform { + @override + String get pathSeparator => isWindows ? r'\' : '/'; +} + const String _correctLicenseFileText = ''' Copyright 2013 The Flutter Authors. All rights reserved. diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart index a9ad510f7ee9..e4a6c5c859e4 100644 --- a/script/tool/test/lint_android_command_test.dart +++ b/script/tool/test/lint_android_command_test.dart @@ -16,7 +16,7 @@ import 'mocks.dart'; import 'util.dart'; void main() { - group('$LintAndroidCommand', () { + group('LintAndroidCommand', () { FileSystem fileSystem; late Directory packagesDir; late CommandRunner runner; @@ -24,7 +24,7 @@ void main() { late RecordingProcessRunner processRunner; setUp(() { - fileSystem = MemoryFileSystem(style: FileSystemStyle.posix); + fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); mockPlatform = MockPlatform(); processRunner = RecordingProcessRunner(); @@ -40,7 +40,7 @@ void main() { }); test('runs gradle lint', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, extraFiles: [ 'example/android/gradlew', ], platformSupport: { @@ -48,7 +48,7 @@ void main() { }); final Directory androidDir = - pluginDir.childDirectory('example').childDirectory('android'); + plugin.getExamples().first.platformDirectory(FlutterPlatform.android); final List output = await runCapturingPrint(runner, ['lint-android']); @@ -72,6 +72,45 @@ void main() { ])); }); + 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: { @@ -89,18 +128,26 @@ void main() { output, containsAllInOrder( [ - contains('Build example before linting'), + contains('Build examples before linting'), ], )); }); test('fails if linting finds issues', () async { - createFakePlugin('plugin1', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }); + final RepositoryPackage plugin = + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'example/android/gradlew', + ], platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline) + }); - processRunner.mockProcessesForExecutable['gradlew'] = [ + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ MockProcess(exitCode: 1), ]; @@ -115,7 +162,7 @@ void main() { output, containsAllInOrder( [ - contains('Build example before linting'), + contains('The following packages had errors:'), ], )); }); @@ -131,7 +178,7 @@ void main() { containsAllInOrder( [ contains( - 'SKIPPING: Plugin does not have an Android implemenatation.') + 'SKIPPING: Plugin does not have an Android implementation.') ], )); }); @@ -150,7 +197,7 @@ void main() { containsAllInOrder( [ contains( - 'SKIPPING: Plugin does not have an Android implemenatation.') + 'SKIPPING: Plugin does not have an Android implementation.') ], )); }); diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index bccbec678666..097bcff338a5 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -23,7 +23,7 @@ void main() { late RecordingProcessRunner processRunner; setUp(() { - fileSystem = MemoryFileSystem(style: FileSystemStyle.posix); + fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); mockPlatform = MockPlatform(isMacOS: true); @@ -65,7 +65,7 @@ void main() { }); test('runs pod lib lint on a podspec', () async { - final Directory plugin1Dir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin1', packagesDir, extraFiles: [ @@ -91,8 +91,8 @@ void main() { [ 'lib', 'lint', - plugin1Dir - .childDirectory('ios') + plugin + .platformDirectory(FlutterPlatform.ios) .childFile('plugin1.podspec') .path, '--configuration=Debug', @@ -106,8 +106,8 @@ void main() { [ 'lib', 'lint', - plugin1Dir - .childDirectory('ios') + plugin + .platformDirectory(FlutterPlatform.ios) .childFile('plugin1.podspec') .path, '--configuration=Debug', diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index fcdf9fafdb63..f19215c89b9e 100644 --- a/script/tool/test/list_command_test.dart +++ b/script/tool/test/list_command_test.dart @@ -12,7 +12,7 @@ import 'mocks.dart'; import 'util.dart'; void main() { - group('$ListCommand', () { + group('ListCommand', () { late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; @@ -29,17 +29,17 @@ void main() { runner.addCommand(command); }); - test('lists plugins', () async { - createFakePlugin('plugin1', packagesDir); + test('lists top-level packages', () async { + createFakePackage('package1', packagesDir); createFakePlugin('plugin2', packagesDir); final List plugins = - await runCapturingPrint(runner, ['list', '--type=plugin']); + await runCapturingPrint(runner, ['list', '--type=package']); expect( plugins, orderedEquals([ - '/packages/plugin1', + '/packages/package1', '/packages/plugin2', ]), ); @@ -64,20 +64,20 @@ void main() { ); }); - test('lists packages', () async { - createFakePlugin('plugin1', packagesDir); + 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']); + final List packages = await runCapturingPrint( + runner, ['list', '--type=package-or-subpackage']); expect( packages, unorderedEquals([ - '/packages/plugin1', - '/packages/plugin1/example', + '/packages/package1', + '/packages/package1/example', '/packages/plugin2', '/packages/plugin2/example/example1', '/packages/plugin2/example/example2', @@ -101,15 +101,18 @@ void main() { '/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', ]), ); }); @@ -119,17 +122,11 @@ void main() { // Create a federated plugin by creating a directory under the packages // directory with several packages underneath. - final Directory federatedPlugin = packagesDir.childDirectory('my_plugin') - ..createSync(); - final Directory clientLibrary = - federatedPlugin.childDirectory('my_plugin')..createSync(); - createFakePubspec(clientLibrary); - final Directory webLibrary = - federatedPlugin.childDirectory('my_plugin_web')..createSync(); - createFakePubspec(webLibrary); - final Directory macLibrary = - federatedPlugin.childDirectory('my_plugin_macos')..createSync(); - createFakePubspec(macLibrary); + 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 = @@ -151,17 +148,11 @@ void main() { // Create a federated plugin by creating a directory under the packages // directory with several packages underneath. - final Directory federatedPlugin = packagesDir.childDirectory('my_plugin') - ..createSync(); - final Directory clientLibrary = - federatedPlugin.childDirectory('my_plugin')..createSync(); - createFakePubspec(clientLibrary); - final Directory webLibrary = - federatedPlugin.childDirectory('my_plugin_web')..createSync(); - createFakePubspec(webLibrary); - final Directory macLibrary = - federatedPlugin.childDirectory('my_plugin_macos')..createSync(); - createFakePubspec(macLibrary); + 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']); diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart index bdd2139b237c..e846a63fc68e 100644 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -7,12 +7,11 @@ 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/repository_package.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/plugin_command_test.mocks.dart'; +import 'common/package_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; @@ -50,7 +49,7 @@ void main() { /// Adds dummy 'dependencies:' entries for each package in [dependencies] /// to [package]. - void _addDependencies( + void addDependencies( RepositoryPackage package, Iterable dependencies) { final List lines = package.pubspecFile.readAsLinesSync(); final int dependenciesStartIndex = lines.indexOf('dependencies:'); @@ -61,11 +60,24 @@ void main() { 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 { - RepositoryPackage(createFakePackage('foo', packagesDir, isFlutter: true)); - final RepositoryPackage packageBar = RepositoryPackage( - createFakePackage('bar', packagesDir, isFlutter: true)); - _addDependencies(packageBar, ['foo']); + createFakePackage('foo', packagesDir, isFlutter: true); + final RepositoryPackage packageBar = + createFakePackage('bar', packagesDir, isFlutter: true); + addDependencies(packageBar, ['foo']); final String originalPubspecContents = packageBar.pubspecFile.readAsStringSync(); @@ -82,28 +94,27 @@ void main() { expect(packageBar.pubspecFile.readAsStringSync(), originalPubspecContents); }); - test('rewrites references', () async { - final RepositoryPackage simplePackage = RepositoryPackage( - createFakePackage('foo', packagesDir, isFlutter: true)); + test('rewrites "dependencies" references', () async { + final RepositoryPackage simplePackage = + createFakePackage('foo', packagesDir, isFlutter: true); final Directory pluginGroup = packagesDir.childDirectory('bar'); - RepositoryPackage(createFakePackage('bar_platform_interface', pluginGroup, - isFlutter: true)); + createFakePackage('bar_platform_interface', pluginGroup, isFlutter: true); final RepositoryPackage pluginImplementation = - RepositoryPackage(createFakePlugin('bar_android', pluginGroup)); + createFakePlugin('bar_android', pluginGroup); final RepositoryPackage pluginAppFacing = - RepositoryPackage(createFakePlugin('bar', pluginGroup)); + createFakePlugin('bar', pluginGroup); - _addDependencies(simplePackage, [ + addDependencies(simplePackage, [ 'bar', 'bar_android', 'bar_platform_interface', ]); - _addDependencies(pluginAppFacing, [ + addDependencies(pluginAppFacing, [ 'bar_platform_interface', 'bar_android', ]); - _addDependencies(pluginImplementation, [ + addDependencies(pluginImplementation, [ 'bar_platform_interface', ]); @@ -144,20 +155,129 @@ void main() { ])); }); + 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 Directory package = createFakePackage('foo', packagesDir); + final RepositoryPackage package = createFakePackage('foo', packagesDir); final String changedFileOutput = [ - package.childFile('pubspec.yaml'), + 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: RepositoryPackage(package).pubspecFile.readAsStringSync()), + MockProcess(stdout: package.pubspecFile.readAsStringSync()), ]; final List output = await runCapturingPrint(runner, [ @@ -198,10 +318,10 @@ void main() { test('includes bugfix version changes as targets', () async { const String newVersion = '1.0.1'; - final Directory package = + final RepositoryPackage package = createFakePackage('foo', packagesDir, version: newVersion); - final File pubspecFile = RepositoryPackage(package).pubspecFile; + final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); @@ -230,10 +350,10 @@ void main() { test('includes minor version changes to 1.0+ as targets', () async { const String newVersion = '1.1.0'; - final Directory package = + final RepositoryPackage package = createFakePackage('foo', packagesDir, version: newVersion); - final File pubspecFile = RepositoryPackage(package).pubspecFile; + final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); @@ -262,10 +382,10 @@ void main() { test('does not include major version changes as targets', () async { const String newVersion = '2.0.0'; - final Directory package = + final RepositoryPackage package = createFakePackage('foo', packagesDir, version: newVersion); - final File pubspecFile = RepositoryPackage(package).pubspecFile; + final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); @@ -294,10 +414,10 @@ void main() { test('does not include minor version changes to 0.x as targets', () async { const String newVersion = '0.8.0'; - final Directory package = + final RepositoryPackage package = createFakePackage('foo', packagesDir, version: newVersion); - final File pubspecFile = RepositoryPackage(package).pubspecFile; + final File pubspecFile = package.pubspecFile; final String changedFileOutput = [ pubspecFile, ].map((File file) => file.path).join('\n'); @@ -323,5 +443,41 @@ void main() { ]), ); }); + + 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/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index 1069a68107c5..f24d014bbfea 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -57,8 +57,8 @@ final Map _kDeviceListMap = { const String _fakeCmakeCommand = 'path/to/cmake'; -void _createFakeCMakeCache(Directory pluginDir, Platform platform) { - final CMakeProject project = CMakeProject(pluginDir.childDirectory('example'), +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); @@ -68,7 +68,7 @@ void _createFakeCMakeCache(Directory pluginDir, Platform platform) { // 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'; + const String kDestination = '--ios-destination'; group('test native_test_command on Posix', () { late FileSystem fileSystem; @@ -95,7 +95,7 @@ void main() { // Returns a MockProcess to provide for "xcrun xcodebuild -list" for a // project that contains [targets]. - MockProcess _getMockXcodebuildListProcess(List targets) { + MockProcess getMockXcodebuildListProcess(List targets) { final Map projects = { 'project': { 'targets': targets, @@ -106,7 +106,7 @@ void main() { // Returns the ProcessCall to expect for checking the targets present in // the [package]'s [platform]/Runner.xcodeproj. - ProcessCall _getTargetCheckCall(Directory package, String platform) { + ProcessCall getTargetCheckCall(Directory package, String platform) { return ProcessCall( 'xcrun', [ @@ -124,7 +124,7 @@ void main() { // Returns the ProcessCall to expect for running the tests in the // workspace [platform]/Runner.xcworkspace, with the given extra flags. - ProcessCall _getRunTestCall( + ProcessCall getRunTestCall( Directory package, String platform, { String? destination, @@ -150,13 +150,12 @@ void main() { // Returns the ProcessCall to expect for build the Linux unit tests for the // given plugin. - ProcessCall _getLinuxBuildCall(Directory pluginDir) { + ProcessCall getLinuxBuildCall(RepositoryPackage plugin) { return ProcessCall( 'cmake', [ '--build', - pluginDir - .childDirectory('example') + getExampleDir(plugin) .childDirectory('build') .childDirectory('linux') .childDirectory('x64') @@ -205,16 +204,15 @@ void main() { }); test('reports skips with no tests', () async { - final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess(['RunnerTests', 'RunnerUITests']), + getMockXcodebuildListProcess(['RunnerTests', 'RunnerUITests']), // Exit code 66 from testing indicates no tests. MockProcess(exitCode: 66), ]; @@ -231,8 +229,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), - _getRunTestCall(pluginExampleDirectory, 'macos', + getTargetCheckCall(pluginExampleDirectory, 'macos'), + getRunTestCall(pluginExampleDirectory, 'macos', extraFlags: ['-only-testing:RunnerUITests']), ])); }); @@ -245,7 +243,7 @@ void main() { }); final List output = await runCapturingPrint(runner, - ['native-test', '--ios', _kDestination, 'foo_destination']); + ['native-test', '--ios', kDestination, 'foo_destination']); expect( output, containsAllInOrder([ @@ -262,7 +260,7 @@ void main() { }); final List output = await runCapturingPrint(runner, - ['native-test', '--ios', _kDestination, 'foo_destination']); + ['native-test', '--ios', kDestination, 'foo_destination']); expect( output, containsAllInOrder([ @@ -273,23 +271,22 @@ void main() { }); test('running with correct destination', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; final List output = await runCapturingPrint(runner, [ 'native-test', '--ios', - _kDestination, + kDestination, 'foo_destination', ]); @@ -303,24 +300,23 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'ios'), - _getRunTestCall(pluginExampleDirectory, 'ios', + getTargetCheckCall(pluginExampleDirectory, 'ios'), + getRunTestCall(pluginExampleDirectory, 'ios', destination: 'foo_destination'), ])); }); test('Not specifying --ios-destination assigns an available simulator', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + 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( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; @@ -340,8 +336,8 @@ void main() { '--json', ], null), - _getTargetCheckCall(pluginExampleDirectory, 'ios'), - _getRunTestCall(pluginExampleDirectory, 'ios', + getTargetCheckCall(pluginExampleDirectory, 'ios'), + getRunTestCall(pluginExampleDirectory, 'ios', destination: 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A'), ])); }); @@ -382,17 +378,15 @@ void main() { }); test('runs for macOS plugin', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; @@ -409,15 +403,15 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), - _getRunTestCall(pluginExampleDirectory, 'macos'), + getTargetCheckCall(pluginExampleDirectory, 'macos'), + getRunTestCall(pluginExampleDirectory, 'macos'), ])); }); }); group('Android', () { test('runs Java unit tests in Android implementation folder', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -431,8 +425,10 @@ void main() { await runCapturingPrint(runner, ['native-test', '--android']); - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); + final Directory androidFolder = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); expect( processRunner.recordedCalls, @@ -447,7 +443,7 @@ void main() { }); test('runs Java unit tests in example folder', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -461,8 +457,10 @@ void main() { await runCapturingPrint(runner, ['native-test', '--android']); - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); + final Directory androidFolder = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); expect( processRunner.recordedCalls, @@ -477,7 +475,7 @@ void main() { }); test('runs Java integration tests', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -492,8 +490,10 @@ void main() { await runCapturingPrint( runner, ['native-test', '--android', '--no-unit']); - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); + final Directory androidFolder = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); expect( processRunner.recordedCalls, @@ -539,7 +539,7 @@ void main() { }); test('runs all tests when present', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -554,8 +554,10 @@ void main() { await runCapturingPrint(runner, ['native-test', '--android']); - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); + final Directory androidFolder = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); expect( processRunner.recordedCalls, @@ -578,7 +580,7 @@ void main() { }); test('honors --no-unit', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -594,8 +596,10 @@ void main() { await runCapturingPrint( runner, ['native-test', '--android', '--no-unit']); - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); + final Directory androidFolder = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); expect( processRunner.recordedCalls, @@ -613,7 +617,7 @@ void main() { }); test('honors --no-integration', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -629,8 +633,10 @@ void main() { await runCapturingPrint( runner, ['native-test', '--android', '--no-integration']); - final Directory androidFolder = - plugin.childDirectory('example').childDirectory('android'); + final Directory androidFolder = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android); expect( processRunner.recordedCalls, @@ -720,7 +726,7 @@ void main() { }); test('fails when a unit test fails', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -732,9 +738,10 @@ void main() { ], ); - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ @@ -761,7 +768,7 @@ void main() { }); test('fails when an integration test fails', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -774,9 +781,10 @@ void main() { ], ); - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ @@ -882,15 +890,15 @@ void main() { test('builds and runs unit tests', () async { const String testBinaryRelativePath = 'build/linux/x64/release/bar/plugin_test'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); - final File testBinary = childFileWithSubcomponents(pluginDirectory, + final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ @@ -910,7 +918,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getLinuxBuildCall(pluginDirectory), + getLinuxBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); @@ -920,17 +928,17 @@ void main() { 'build/linux/x64/debug/bar/plugin_test'; const String releaseTestBinaryRelativePath = 'build/linux/x64/release/bar/plugin_test'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$debugTestBinaryRelativePath', 'example/$releaseTestBinaryRelativePath' ], platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); final File releaseTestBinary = childFileWithSubcomponents( - pluginDirectory, + plugin.directory, ['example', ...releaseTestBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ @@ -950,7 +958,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getLinuxBuildCall(pluginDirectory), + getLinuxBuildCall(plugin), ProcessCall(releaseTestBinary.path, const [], null), ])); }); @@ -983,12 +991,11 @@ void main() { }); test('fails if there are no unit tests', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); Error? commandError; final List output = await runCapturingPrint(runner, [ @@ -1010,22 +1017,22 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getLinuxBuildCall(pluginDirectory), + getLinuxBuildCall(plugin), ])); }); test('fails if a unit test fails', () async { const String testBinaryRelativePath = 'build/linux/x64/release/bar/plugin_test'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); - final File testBinary = childFileWithSubcomponents(pluginDirectory, + final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); processRunner.mockProcessesForExecutable[testBinary.path] = @@ -1051,7 +1058,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getLinuxBuildCall(pluginDirectory), + getLinuxBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); @@ -1087,17 +1094,15 @@ void main() { }); test('honors unit-only', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; @@ -1116,24 +1121,23 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), - _getRunTestCall(pluginExampleDirectory, 'macos', + getTargetCheckCall(pluginExampleDirectory, 'macos'), + getRunTestCall(pluginExampleDirectory, 'macos', extraFlags: ['-only-testing:RunnerTests']), ])); }); test('honors integration-only', () async { - final Directory pluginDirectory1 = createFakePlugin( + final RepositoryPackage plugin1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin1); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; @@ -1152,25 +1156,24 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), - _getRunTestCall(pluginExampleDirectory, 'macos', + getTargetCheckCall(pluginExampleDirectory, 'macos'), + getRunTestCall(pluginExampleDirectory, 'macos', extraFlags: ['-only-testing:RunnerUITests']), ])); }); test('skips when the requested target is not present', () async { - final Directory pluginDirectory1 = createFakePlugin( + final RepositoryPackage plugin1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin1); // Simulate a project with unit tests but no integration tests... processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess(['RunnerTests']), + getMockXcodebuildListProcess(['RunnerTests']), ]; // ... then try to run only integration tests. @@ -1190,22 +1193,21 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), + getTargetCheckCall(pluginExampleDirectory, 'macos'), ])); }); test('fails if there are no unit tests', () async { - final Directory pluginDirectory1 = createFakePlugin( + final RepositoryPackage plugin1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin1); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess(['RunnerUITests']), + getMockXcodebuildListProcess(['RunnerUITests']), ]; Error? commandError; @@ -1230,19 +1232,18 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), + getTargetCheckCall(pluginExampleDirectory, 'macos'), ])); }); test('fails if unable to check for requested target', () async { - final Directory pluginDirectory1 = createFakePlugin( + final RepositoryPackage plugin1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin1); processRunner.mockProcessesForExecutable['xcrun'] = [ MockProcess(exitCode: 1), // xcodebuild -list @@ -1268,14 +1269,14 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), + getTargetCheckCall(pluginExampleDirectory, 'macos'), ])); }); }); group('multiplatform', () { test('runs all platfroms when supported', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -1289,16 +1290,15 @@ void main() { }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final Directory androidFolder = pluginExampleDirectory.childDirectory('android'); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), // iOS list MockProcess(), // iOS run - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), // macOS list MockProcess(), // macOS run ]; @@ -1308,7 +1308,7 @@ void main() { '--android', '--ios', '--macos', - _kDestination, + kDestination, 'foo_destination', ]); @@ -1325,26 +1325,24 @@ void main() { orderedEquals([ ProcessCall(androidFolder.childFile('gradlew').path, const ['testDebugUnitTest'], androidFolder.path), - _getTargetCheckCall(pluginExampleDirectory, 'ios'), - _getRunTestCall(pluginExampleDirectory, 'ios', + getTargetCheckCall(pluginExampleDirectory, 'ios'), + getRunTestCall(pluginExampleDirectory, 'ios', destination: 'foo_destination'), - _getTargetCheckCall(pluginExampleDirectory, 'macos'), - _getRunTestCall(pluginExampleDirectory, 'macos'), + getTargetCheckCall(pluginExampleDirectory, 'macos'), + getRunTestCall(pluginExampleDirectory, 'macos'), ])); }); test('runs only macOS for a macOS plugin', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; @@ -1352,7 +1350,7 @@ void main() { 'native-test', '--ios', '--macos', - _kDestination, + kDestination, 'foo_destination', ]); @@ -1366,22 +1364,21 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'macos'), - _getRunTestCall(pluginExampleDirectory, 'macos'), + getTargetCheckCall(pluginExampleDirectory, 'macos'), + getRunTestCall(pluginExampleDirectory, 'macos'), ])); }); test('runs only iOS for a iOS plugin', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; @@ -1389,7 +1386,7 @@ void main() { 'native-test', '--ios', '--macos', - _kDestination, + kDestination, 'foo_destination', ]); @@ -1403,8 +1400,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getTargetCheckCall(pluginExampleDirectory, 'ios'), - _getRunTestCall(pluginExampleDirectory, 'ios', + getTargetCheckCall(pluginExampleDirectory, 'ios'), + getRunTestCall(pluginExampleDirectory, 'ios', destination: 'foo_destination'), ])); }); @@ -1418,7 +1415,7 @@ void main() { '--ios', '--macos', '--windows', - _kDestination, + kDestination, 'foo_destination', ]); @@ -1450,7 +1447,7 @@ void main() { 'native-test', '--macos', '--windows', - _kDestination, + kDestination, 'foo_destination', ]); @@ -1466,7 +1463,7 @@ void main() { }); test('failing one platform does not stop the tests', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -1480,14 +1477,15 @@ void main() { ); processRunner.mockProcessesForExecutable['xcrun'] = [ - _getMockXcodebuildListProcess( + getMockXcodebuildListProcess( ['RunnerTests', 'RunnerUITests']), ]; // Simulate failing Android, but not iOS. - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ @@ -1522,7 +1520,7 @@ void main() { }); test('failing multiple platforms reports multiple failures', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -1536,9 +1534,10 @@ void main() { ); // Simulate failing Android. - final String gradlewPath = pluginDir - .childDirectory('example') - .childDirectory('android') + final String gradlewPath = plugin + .getExamples() + .first + .platformDirectory(FlutterPlatform.android) .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ @@ -1599,13 +1598,12 @@ void main() { // Returns the ProcessCall to expect for build the Windows unit tests for // the given plugin. - ProcessCall _getWindowsBuildCall(Directory pluginDir) { + ProcessCall getWindowsBuildCall(RepositoryPackage plugin) { return ProcessCall( _fakeCmakeCommand, [ '--build', - pluginDir - .childDirectory('example') + getExampleDir(plugin) .childDirectory('build') .childDirectory('windows') .path, @@ -1621,15 +1619,15 @@ void main() { test('runs unit tests', () async { const String testBinaryRelativePath = 'build/windows/Debug/bar/plugin_test.exe'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); - final File testBinary = childFileWithSubcomponents(pluginDirectory, + final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ @@ -1649,7 +1647,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getWindowsBuildCall(pluginDirectory), + getWindowsBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); @@ -1659,16 +1657,17 @@ void main() { 'build/windows/Debug/bar/plugin_test.exe'; const String releaseTestBinaryRelativePath = 'build/windows/Release/bar/plugin_test.exe'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$debugTestBinaryRelativePath', 'example/$releaseTestBinaryRelativePath' ], platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); - final File debugTestBinary = childFileWithSubcomponents(pluginDirectory, + final File debugTestBinary = childFileWithSubcomponents( + plugin.directory, ['example', ...debugTestBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ @@ -1688,7 +1687,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getWindowsBuildCall(pluginDirectory), + getWindowsBuildCall(plugin), ProcessCall(debugTestBinary.path, const [], null), ])); }); @@ -1721,12 +1720,11 @@ void main() { }); test('fails if there are no unit tests', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); Error? commandError; final List output = await runCapturingPrint(runner, [ @@ -1748,22 +1746,22 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getWindowsBuildCall(pluginDirectory), + getWindowsBuildCall(plugin), ])); }); test('fails if a unit test fails', () async { const String testBinaryRelativePath = 'build/windows/Debug/bar/plugin_test.exe'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(pluginDirectory, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform); - final File testBinary = childFileWithSubcomponents(pluginDirectory, + final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); processRunner.mockProcessesForExecutable[testBinary.path] = @@ -1789,7 +1787,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - _getWindowsBuildCall(pluginDirectory), + getWindowsBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index c5527af21736..575f8509fd25 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -44,10 +44,16 @@ void main() { }); test('publish check all packages', () async { - final Directory plugin1Dir = - createFakePlugin('plugin_tools_test_package_a', packagesDir); - final Directory plugin2Dir = - createFakePlugin('plugin_tools_test_package_b', packagesDir); + 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']); @@ -57,14 +63,57 @@ void main() { ProcessCall( 'flutter', const ['pub', 'publish', '--', '--dry-run'], - plugin1Dir.path), + plugin1.path), ProcessCall( 'flutter', const ['pub', 'publish', '--', '--dry-run'], - plugin2Dir.path), + 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); @@ -89,8 +138,8 @@ void main() { }); test('fail on bad pubspec', () async { - final Directory dir = createFakePlugin('c', packagesDir); - await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); + final RepositoryPackage package = createFakePlugin('c', packagesDir); + await package.pubspecFile.writeAsString('bad-yaml'); Error? commandError; final List output = await runCapturingPrint( @@ -108,8 +157,9 @@ void main() { }); test('fails if AUTHORS is missing', () async { - final Directory package = createFakePackage('a_package', packagesDir); - package.childFile('AUTHORS').delete(); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + package.authorsFile.delete(); Error? commandError; final List output = await runCapturingPrint( @@ -128,12 +178,12 @@ void main() { }); test('does not require AUTHORS for third-party', () async { - final Directory package = createFakePackage( + final RepositoryPackage package = createFakePackage( 'a_package', packagesDir.parent .childDirectory('third_party') .childDirectory('packages')); - package.childFile('AUTHORS').delete(); + package.authorsFile.delete(); final List output = await runCapturingPrint(runner, ['publish-check']); @@ -372,11 +422,11 @@ void main() { ); runner.addCommand(command); - final Directory plugin1Dir = + final RepositoryPackage plugin = createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); + await plugin.pubspecFile.writeAsString('bad-yaml'); bool hasError = false; final List output = await runCapturingPrint( diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_command_test.dart similarity index 81% rename from script/tool/test/publish_plugin_command_test.dart rename to script/tool/test/publish_command_test.dart index 2cb3fc25af2e..da5f9c871f05 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_command_test.dart @@ -10,14 +10,14 @@ 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_plugin_command.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/plugin_command_test.mocks.dart'; +import 'common/package_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; @@ -33,8 +33,8 @@ void main() { // Map of package name to mock response. late Map> mockHttpResponses; - void _createMockCredentialFile() { - final String credentialPath = PublishPluginCommand.getCredentialPath(); + void createMockCredentialFile() { + final String credentialPath = PublishCommand.getCredentialPath(); fileSystem.file(credentialPath) ..createSync(recursive: true) ..writeAsStringSync('some credential'); @@ -72,7 +72,7 @@ void main() { mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') - ..addCommand(PublishPluginCommand( + ..addCommand(PublishCommand( packagesDir, processRunner: processRunner, stdinput: mockStdin, @@ -83,17 +83,17 @@ void main() { group('Initial validation', () { test('refuses to proceed with dirty files', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); processRunner.mockProcessesForExecutable['git-status'] = [ - MockProcess(stdout: '?? ${pluginDir.childFile('tmp').path}\n') + MockProcess(stdout: '?? ${plugin.directory.childFile('tmp').path}\n') ]; Error? commandError; final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', ], errorHandler: (Error e) { commandError = e; @@ -103,7 +103,7 @@ void main() { expect( output, containsAllInOrder([ - contains('There are files in the package directory that haven\'t ' + 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 && ' @@ -113,7 +113,7 @@ void main() { ])); }); - test('fails immediately if the remote doesn\'t exist', () async { + test("fails immediately if the remote doesn't exist", () async { createFakePlugin('foo', packagesDir, examples: []); processRunner.mockProcessesForExecutable['git-remote'] = [ @@ -122,7 +122,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint( - commandRunner, ['publish-plugin', '--packages=foo'], + commandRunner, ['publish', '--packages=foo'], errorHandler: (Error e) { commandError = e; }); @@ -154,8 +154,8 @@ void main() { stderrEncoding: utf8), // pub publish for plugin1 ]; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--packages=plugin1,plugin2']); + final List output = await runCapturingPrint( + commandRunner, ['publish', '--packages=plugin1,plugin2']); expect( output, @@ -176,18 +176,18 @@ void main() { mockStdin.mockUserInputs.add(utf8.encode('user input')); await runCapturingPrint( - commandRunner, ['publish-plugin', '--packages=foo']); + commandRunner, ['publish', '--packages=foo']); expect(processRunner.mockPublishProcess.stdinMock.lines, contains('user input')); }); test('forwards --pub-publish-flags to pub publish', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', '--pub-publish-flags', '--dry-run,--server=bar' @@ -198,18 +198,18 @@ void main() { contains(ProcessCall( flutterCommand, const ['pub', 'publish', '--dry-run', '--server=bar'], - pluginDir.path))); + plugin.path))); }); test( '--skip-confirmation flag automatically adds --force to --pub-publish-flags', () async { - _createMockCredentialFile(); - final Directory pluginDir = + createMockCredentialFile(); + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', '--skip-confirmation', '--pub-publish-flags', @@ -221,7 +221,36 @@ void main() { contains(ProcessCall( flutterCommand, const ['pub', 'publish', '--server=bar', '--force'], - pluginDir.path))); + 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 { @@ -234,7 +263,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', ], errorHandler: (Error e) { commandError = e; @@ -249,12 +278,12 @@ void main() { }); test('publish, dry run', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', '--dry-run', ]); @@ -268,7 +297,7 @@ void main() { containsAllInOrder([ contains('=============== DRY RUN ==============='), contains('Running for foo'), - contains('Running `pub publish ` in ${pluginDir.path}...'), + contains('Running `pub publish ` in ${plugin.path}...'), contains('Tagging release foo-v0.0.1...'), contains('Pushing tag to upstream...'), contains('Published foo successfully!'), @@ -281,7 +310,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=$packageName', ]); @@ -301,7 +330,7 @@ void main() { test('with the version and name from the pubspec.yaml', () async { createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', ]); @@ -319,7 +348,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', ], errorHandler: (Error e) { commandError = e; @@ -346,7 +375,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', ]); @@ -364,12 +393,12 @@ void main() { test('does not ask for user input if the --skip-confirmation flag is on', () async { - _createMockCredentialFile(); + createMockCredentialFile(); createFakePlugin('foo', packagesDir, examples: []); final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--skip-confirmation', '--packages=foo', ]); @@ -386,13 +415,13 @@ void main() { }); test('to upstream by default, dry run', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); mockStdin.readLineOutput = 'y'; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--packages=foo', '--dry-run']); + final List output = await runCapturingPrint( + commandRunner, ['publish', '--packages=foo', '--dry-run']); expect( processRunner.recordedCalls @@ -402,7 +431,7 @@ void main() { output, containsAllInOrder([ contains('=============== DRY RUN ==============='), - contains('Running `pub publish ` in ${pluginDir.path}...'), + contains('Running `pub publish ` in ${plugin.path}...'), contains('Tagging release foo-v0.0.1...'), contains('Pushing tag to upstream...'), contains('Published foo successfully!'), @@ -416,7 +445,7 @@ void main() { final List output = await runCapturingPrint(commandRunner, [ - 'publish-plugin', + 'publish', '--packages=foo', '--remote', 'origin', @@ -447,29 +476,30 @@ void main() { }; // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = createFakePlugin( + final RepositoryPackage plugin2 = createFakePlugin( 'plugin2', packagesDir.childDirectory('plugin2'), ); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output, containsAllInOrder([ contains( 'Publishing all packages that have changed relative to "HEAD~"'), - contains('Running `pub publish ` in ${pluginDir1.path}...'), - contains('Running `pub publish ` in ${pluginDir2.path}...'), + 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'), ])); @@ -503,9 +533,10 @@ void main() { // The existing plugin. createFakePlugin('plugin0', packagesDir); // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = + final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); // Git results for plugin0 having been released already, and plugin1 and @@ -515,20 +546,20 @@ void main() { ]; processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output, containsAllInOrder([ - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Running `pub publish ` in ${plugin1.path}...\n', + 'Running `pub publish ` in ${plugin2.path}...\n', ])); expect( processRunner.recordedCalls, @@ -552,21 +583,22 @@ void main() { }; // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = + final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint( commandRunner, [ - 'publish-plugin', + 'publish', '--all-changed', '--base-sha=HEAD~', '--dry-run' @@ -576,11 +608,11 @@ void main() { output, containsAllInOrder([ contains('=============== DRY RUN ==============='), - contains('Running `pub publish ` in ${pluginDir1.path}...'), + 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 ${pluginDir2.path}...'), + contains('Running `pub publish ` in ${plugin2.path}...'), contains('Tagging release plugin2-v0.0.1...'), contains('Pushing tag to upstream...'), contains('Published plugin2 successfully!'), @@ -603,29 +635,29 @@ void main() { }; // Non-federated - final Directory pluginDir1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - final Directory pluginDir2 = createFakePlugin( + final RepositoryPackage plugin2 = createFakePlugin( 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; mockStdin.readLineOutput = 'y'; final List output2 = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output2, containsAllInOrder([ - contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Running `pub publish ` in ${plugin1.path}...'), contains('Published plugin1 successfully!'), - contains('Running `pub publish ` in ${pluginDir2.path}...'), + contains('Running `pub publish ` in ${plugin2.path}...'), contains('Published plugin2 successfully!'), ])); expect( @@ -652,27 +684,27 @@ void main() { }; // Non-federated - final Directory pluginDir1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - final Directory pluginDir2 = + final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - pluginDir2.deleteSync(recursive: true); + plugin2.directory.deleteSync(recursive: true); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; mockStdin.readLineOutput = 'y'; final List output2 = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output2, containsAllInOrder([ - contains('Running `pub publish ` in ${pluginDir1.path}...'), + 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'), @@ -698,17 +730,17 @@ void main() { }; // Non-federated - final Directory pluginDir1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - final Directory pluginDir2 = createFakePlugin( + final RepositoryPackage plugin2 = createFakePlugin( 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; processRunner.mockProcessesForExecutable['git-tag'] = [ MockProcess( @@ -717,7 +749,7 @@ void main() { ]; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output, @@ -748,22 +780,22 @@ void main() { }; // Non-federated - final Directory pluginDir1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - final Directory pluginDir2 = createFakePlugin( + final RepositoryPackage plugin2 = createFakePlugin( 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' - '${pluginDir2.childFile('pubspec.yaml').path}\n') + stdout: '${plugin1.pubspecFile.path}\n' + '${plugin2.pubspecFile.path}\n') ]; Error? commandError; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~'], + ['publish', '--all-changed', '--base-sha=HEAD~'], errorHandler: (Error e) { commandError = e; }); @@ -785,19 +817,20 @@ void main() { test('No version change does not release any plugins', () async { // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); // federated - final Directory pluginDir2 = + final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( - stdout: '${pluginDir1.childFile('plugin1.dart').path}\n' - '${pluginDir2.childFile('plugin2.dart').path}\n') + stdout: '${plugin1.libDirectory.childFile('plugin1.dart').path}\n' + '${plugin2.libDirectory.childFile('plugin2.dart').path}\n') ]; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect(output, containsAllInOrder(['Ran for 0 package(s)'])); expect( @@ -812,14 +845,14 @@ void main() { 'versions': [], }; - final Directory flutterPluginTools = + final RepositoryPackage flutterPluginTools = createFakePlugin('flutter_plugin_tools', packagesDir); processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: flutterPluginTools.childFile('pubspec.yaml').path) + MockProcess(stdout: flutterPluginTools.pubspecFile.path) ]; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + ['publish', '--all-changed', '--base-sha=HEAD~']); expect( output, @@ -873,8 +906,8 @@ class MockStdin extends Mock implements io.Stdin { } @override - StreamSubscription> listen(void onData(List event)?, - {Function? onError, void onDone()?, bool? cancelOnError}) { + 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); } diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 42d20240da87..2c254ca94984 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -27,6 +27,7 @@ String _headerSection( String name, { bool isPlugin = false, bool includeRepository = true, + String repositoryBranch = 'main', String? repositoryPackagesDirRelativePath, bool includeHomepage = false, bool includeIssueTracker = true, @@ -38,12 +39,12 @@ String _headerSection( 'flutter', if (isPlugin) 'plugins' else 'packages', 'tree', - 'main', + repositoryBranch, 'packages', repositoryPath, ]; final String repoLink = - 'https://github.com/' + repoLinkPathComponents.join('/'); + '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 ' @@ -55,7 +56,7 @@ ${includeRepository ? 'repository: $repoLink' : ''} ${includeHomepage ? 'homepage: $repoLink' : ''} ${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} version: 1.0.0 -${publishable ? '' : 'publish_to: \'none\''} +${publishable ? '' : "publish_to: 'none'"} '''; } @@ -146,15 +147,27 @@ void main() { }); test('passes for a plugin following conventions', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + 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, [ @@ -172,15 +185,28 @@ ${_falseSecretsSection()} }); test('passes for a Flutter package following conventions', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${_headerSection('plugin')} + 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, [ @@ -190,18 +216,18 @@ ${_falseSecretsSection()} expect( output, containsAllInOrder([ - contains('Running for plugin...'), - contains('Running for plugin/example...'), + 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 Directory packageDirectory = packagesDir.childDirectory('package'); - packageDirectory.createSync(recursive: true); + final RepositoryPackage package = + createFakePackage('package', packagesDir, examples: []); - packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + package.pubspecFile.writeAsStringSync(''' ${_headerSection('package')} ${_environmentSection()} ${_dependenciesSection()} @@ -221,9 +247,10 @@ ${_dependenciesSection()} }); test('fails when homepage is included', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeHomepage: true)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -248,9 +275,10 @@ ${_devDependenciesSection()} }); test('fails when repository is missing', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeRepository: false)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -274,9 +302,10 @@ ${_devDependenciesSection()} }); test('fails when homepage is given instead of repository', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -300,10 +329,11 @@ ${_devDependenciesSection()} ); }); - test('fails when repository is incorrect', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + test('fails when repository package name is incorrect', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -326,10 +356,38 @@ ${_devDependenciesSection()} ); }); + 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 Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, includeIssueTracker: false)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -353,10 +411,11 @@ ${_devDependenciesSection()} }); test('fails when description is too short', () async { - final Directory pluginDirectory = - createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); + final RepositoryPackage plugin = createFakePlugin( + 'a_plugin', packagesDir.childDirectory('a_plugin'), + examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, description: 'Too short')} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -383,9 +442,10 @@ ${_devDependenciesSection()} test( 'allows short descriptions for non-app-facing parts of federated plugins', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, description: 'Too short')} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -410,14 +470,15 @@ ${_devDependenciesSection()} }); test('fails when description is too long', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + 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.'; - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true, description: description)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -442,9 +503,10 @@ ${_devDependenciesSection()} }); test('fails when environment section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} ${_flutterSection(isPlugin: true)} ${_dependenciesSection()} @@ -469,9 +531,10 @@ ${_environmentSection()} }); test('fails when flutter section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} ${_flutterSection(isPlugin: true)} ${_environmentSection()} @@ -496,9 +559,10 @@ ${_devDependenciesSection()} }); test('fails when dependencies section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -523,9 +587,9 @@ ${_dependenciesSection()} }); test('fails when dev_dependencies section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} ${_environmentSection()} ${_devDependenciesSection()} @@ -550,9 +614,10 @@ ${_dependenciesSection()} }); test('fails when false_secrets section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin', isPlugin: true)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -579,10 +644,11 @@ ${_devDependenciesSection()} test('fails when an implemenation package is missing "implements"', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + final RepositoryPackage plugin = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin_a_foo', isPlugin: true)} ${_environmentSection()} ${_flutterSection(isPlugin: true)} @@ -607,10 +673,11 @@ ${_devDependenciesSection()} test('fails when an implemenation package has the wrong "implements"', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + final RepositoryPackage plugin = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin_a_foo', isPlugin: true)} ${_environmentSection()} ${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} @@ -635,10 +702,11 @@ ${_devDependenciesSection()} }); test('passes for a correct implemenation package', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + final RepositoryPackage plugin = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection( 'plugin_a_foo', isPlugin: true, @@ -663,10 +731,11 @@ ${_devDependenciesSection()} }); test('fails when a "default_package" looks incorrect', () async { - final Directory pluginDirectory = - createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + final RepositoryPackage plugin = createFakePlugin( + 'plugin_a', packagesDir.childDirectory('plugin_a'), + examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection( 'plugin_a', isPlugin: true, @@ -702,10 +771,11 @@ ${_devDependenciesSection()} test( 'fails when a "default_package" does not have a corresponding dependency', () async { - final Directory pluginDirectory = - createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + final RepositoryPackage plugin = createFakePlugin( + 'plugin_a', packagesDir.childDirectory('plugin_a'), + examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection( 'plugin_a', isPlugin: true, @@ -739,10 +809,11 @@ ${_devDependenciesSection()} }); test('passes for an app-facing package without "implements"', () async { - final Directory pluginDirectory = - createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + final RepositoryPackage plugin = createFakePlugin( + 'plugin_a', packagesDir.childDirectory('plugin_a'), + examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection( 'plugin_a', isPlugin: true, @@ -768,11 +839,11 @@ ${_devDependenciesSection()} test('passes for a platform interface package without "implements"', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin_a_platform_interface', - packagesDir.childDirectory('plugin_a')); + final RepositoryPackage plugin = createFakePlugin( + 'plugin_a_platform_interface', packagesDir.childDirectory('plugin_a'), + examples: []); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection( 'plugin_a_platform_interface', isPlugin: true, @@ -798,12 +869,13 @@ ${_devDependenciesSection()} }); test('validates some properties even for unpublished packages', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + final RepositoryPackage plugin = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); // Environment section is in the wrong location. // Missing 'implements'. - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection('plugin_a_foo', isPlugin: true, publishable: false)} ${_flutterSection(isPlugin: true)} ${_dependenciesSection()} @@ -829,11 +901,12 @@ ${_environmentSection()} }); test('ignores some checks for unpublished packages', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); // Missing metadata that is only useful for published packages, such as // repository and issue tracker. - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + plugin.pubspecFile.writeAsStringSync(''' ${_headerSection( 'plugin', isPlugin: true, @@ -885,10 +958,10 @@ ${_devDependenciesSection()} }); test('repository check works', () async { - final Directory packageDirectory = - createFakePackage('package', packagesDir); + final RepositoryPackage package = + createFakePackage('package', packagesDir, examples: []); - packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' + package.pubspecFile.writeAsStringSync(''' ${_headerSection('package')} ${_environmentSection()} ${_dependenciesSection()} diff --git a/script/tool/test/readme_check_command_test.dart b/script/tool/test/readme_check_command_test.dart new file mode 100644 index 000000000000..eb2b6c8e7512 --- /dev/null +++ b/script/tool/test/readme_check_command_test.dart @@ -0,0 +1,741 @@ +// 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 new file mode 100644 index 000000000000..776cbf197838 --- /dev/null +++ b/script/tool/test/remove_dev_dependencies_test.dart @@ -0,0 +1,102 @@ +// 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 index 9bcd8d1ae67a..14a1e4a67c1f 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -40,9 +40,9 @@ void main() { }); test('runs flutter test on each plugin', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, extraFiles: ['test/empty_test.dart']); - final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir, extraFiles: ['test/empty_test.dart']); await runCapturingPrint(runner, ['test']); @@ -51,9 +51,29 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin1Dir.path), + const ['test', '--color'], plugin1.path), ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin2Dir.path), + 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), ]), ); }); @@ -88,7 +108,7 @@ void main() { test('skips testing plugins without test directory', () async { createFakePlugin('plugin1', packagesDir); - final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir, extraFiles: ['test/empty_test.dart']); await runCapturingPrint(runner, ['test']); @@ -97,15 +117,15 @@ void main() { processRunner.recordedCalls, orderedEquals([ ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin2Dir.path), + const ['test', '--color'], plugin2.path), ]), ); }); - test('runs pub run test on non-Flutter packages', () async { - final Directory pluginDir = createFakePlugin('a', packagesDir, + test('runs dart run test on non-Flutter packages', () async { + final RepositoryPackage plugin = createFakePlugin('a', packagesDir, extraFiles: ['test/empty_test.dart']); - final Directory packageDir = createFakePackage('b', packagesDir, + final RepositoryPackage package = createFakePackage('b', packagesDir, extraFiles: ['test/empty_test.dart']); await runCapturingPrint( @@ -117,12 +137,34 @@ void main() { ProcessCall( getFlutterCommand(mockPlatform), const ['test', '--color', '--enable-experiment=exp1'], - pluginDir.path), - ProcessCall('dart', const ['pub', 'get'], packageDir.path), + plugin.path), + ProcessCall('dart', const ['pub', 'get'], package.path), ProcessCall( 'dart', - const ['pub', 'run', '--enable-experiment=exp1', 'test'], - packageDir.path), + 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), ]), ); }); @@ -176,7 +218,7 @@ void main() { }); test('runs on Chrome for web plugins', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: ['test/empty_test.dart'], @@ -193,15 +235,15 @@ void main() { ProcessCall( getFlutterCommand(mockPlatform), const ['test', '--color', '--platform=chrome'], - pluginDir.path), + plugin.path), ]), ); }); test('enable-experiment flag', () async { - final Directory pluginDir = createFakePlugin('a', packagesDir, + final RepositoryPackage plugin = createFakePlugin('a', packagesDir, extraFiles: ['test/empty_test.dart']); - final Directory packageDir = createFakePackage('b', packagesDir, + final RepositoryPackage package = createFakePackage('b', packagesDir, extraFiles: ['test/empty_test.dart']); await runCapturingPrint( @@ -213,12 +255,12 @@ void main() { ProcessCall( getFlutterCommand(mockPlatform), const ['test', '--color', '--enable-experiment=exp1'], - pluginDir.path), - ProcessCall('dart', const ['pub', 'get'], packageDir.path), + plugin.path), + ProcessCall('dart', const ['pub', 'get'], package.path), ProcessCall( 'dart', - const ['pub', 'run', '--enable-experiment=exp1', 'test'], - packageDir.path), + 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 new file mode 100644 index 000000000000..79f53d8779bb --- /dev/null +++ b/script/tool/test/update_excerpts_command_test.dart @@ -0,0 +1,280 @@ +// 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 files are changed with --fail-on-change', () async { + createFakePlugin('a_plugin', packagesDir, + extraFiles: [kReadmeExcerptConfigPath]); + + const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; + 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'), + ])); + }); + + 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 new file mode 100644 index 000000000000..8cd2e9591e70 --- /dev/null +++ b/script/tool/test/update_release_info_command_test.dart @@ -0,0 +1,645 @@ +// 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/test/plugin_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 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 index 91d21c1d9bb3..a8cb527d9238 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -13,6 +13,7 @@ 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'; @@ -20,6 +21,18 @@ 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) => @@ -47,7 +60,6 @@ Directory createPackagesDirectory( class PlatformDetails { const PlatformDetails( this.type, { - this.variants = const [], this.hasNativeCode = true, this.hasDartCode = false, }); @@ -55,9 +67,6 @@ class PlatformDetails { /// The type of support for the platform. final PlatformSupport type; - /// Any 'supportVariants' to list in the pubspec. - final List variants; - /// Whether or not the plugin includes native code. /// /// Ignored for web, which does not have native code. @@ -69,6 +78,20 @@ class PlatformDetails { 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 @@ -76,8 +99,7 @@ class PlatformDetails { /// /// [extraFiles] is an optional list of plugin-relative paths, using Posix /// separators, of extra files to create in the plugin. -// TODO(stuartmorgan): Convert the return to a RepositoryPackage. -Directory createFakePlugin( +RepositoryPackage createFakePlugin( String name, Directory parentDirectory, { List examples = const ['example'], @@ -85,134 +107,179 @@ Directory createFakePlugin( Map platformSupport = const {}, String? version = '0.0.1', + String flutterConstraint = _defaultFlutterConstraint, + String dartConstraint = _defaultDartConstraint, }) { - final Directory pluginDirectory = createFakePackage(name, parentDirectory, - isFlutter: true, - examples: examples, - extraFiles: extraFiles, - version: version); + final RepositoryPackage package = createFakePackage( + name, + parentDirectory, + isFlutter: true, + examples: examples, + extraFiles: extraFiles, + version: version, + flutterConstraint: flutterConstraint, + dartConstraint: dartConstraint, + ); createFakePubspec( - pluginDirectory, + package, name: name, - isFlutter: true, isPlugin: true, platformSupport: platformSupport, version: version, + flutterConstraint: flutterConstraint, + dartConstraint: dartConstraint, ); - return pluginDirectory; + 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. -// TODO(stuartmorgan): Convert the return to a RepositoryPackage. -Directory createFakePackage( +/// +/// 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 Directory packageDirectory = parentDirectory.childDirectory(name); - packageDirectory.createSync(recursive: true); - - createFakePubspec(packageDirectory, - name: name, isFlutter: isFlutter, version: version); - createFakeCHANGELOG(packageDirectory, ''' + 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. '''); - createFakeAuthors(packageDirectory); + package.readmeFile.writeAsStringSync('A very useful package'); + package.authorsFile.writeAsStringSync('Google Inc.'); + } if (examples.length == 1) { - final Directory exampleDir = packageDirectory.childDirectory(examples.first) - ..createSync(); - createFakePubspec(exampleDir, - name: '${name}_example', isFlutter: isFlutter, publishTo: 'none'); + 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 exampleDir = packageDirectory.childDirectory('example') - ..createSync(); - for (final String example in examples) { - final Directory currentExample = exampleDir.childDirectory(example) - ..createSync(); - createFakePubspec(currentExample, - name: example, isFlutter: isFlutter, publishTo: 'none'); + 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(packageDirectory, posixContext.split(file)) + childFileWithSubcomponents(package.directory, posixContext.split(file)) .createSync(recursive: true); } - return packageDirectory; + return package; } -void createFakeCHANGELOG(Directory parent, String texts) { - parent.childFile('CHANGELOG.md').createSync(); - parent.childFile('CHANGELOG.md').writeAsStringSync(texts); -} - -/// Creates a `pubspec.yaml` file with a flutter dependency. +/// 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( - Directory parent, { + RepositoryPackage package, { String name = 'fake_package', bool isFlutter = true, bool isPlugin = false, Map platformSupport = const {}, - String publishTo = 'http://no_pub_server.com', + String? publishTo, String? version, + String dartConstraint = _defaultDartConstraint, + String flutterConstraint = _defaultFlutterConstraint, }) { isPlugin |= platformSupport.isNotEmpty; - parent.childFile('pubspec.yaml').createSync(); - String yaml = ''' -name: $name + + 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) { - yaml += ''' + pluginSection += ''' flutter: plugin: platforms: '''; for (final MapEntry platform in platformSupport.entries) { - yaml += _pluginPlatformSection(platform.key, platform.value, name); + pluginSection += + _pluginPlatformSection(platform.key, platform.value, name); } } - yaml += ''' -dependencies: - flutter: - sdk: flutter -'''; } - if (version != null) { - yaml += ''' -version: $version -'''; - } - if (publishTo.isNotEmpty) { - yaml += ''' -publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being published by a broken test. + + // 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 '''; - } - parent.childFile('pubspec.yaml').writeAsStringSync(yaml); -} -void createFakeAuthors(Directory parent) { - final File authorsFile = parent.childFile('AUTHORS'); - authorsFile.createSync(); - authorsFile.writeAsStringSync('Google Inc.'); + package.pubspecFile.createSync(); + package.pubspecFile.writeAsStringSync(yaml); } String _pluginPlatformSection( @@ -256,32 +323,21 @@ String _pluginPlatformSection( assert(false, 'Unrecognized platform: $platform'); break; } - entry = lines.join('\n') + '\n'; - } - - // Add any variants. - if (support.variants.isNotEmpty) { - entry += ''' - supportedVariants: -'''; - for (final String variant in support.variants) { - entry += ''' - - $variant -'''; - } + entry = '${lines.join('\n')}\n'; } return entry; } -typedef _ErrorHandler = void Function(Error error); - /// 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, - {_ErrorHandler? errorHandler}) async { + CommandRunner runner, + List args, { + Function(Error error)? errorHandler, + Function(Exception error)? exceptionHandler, +}) async { final List prints = []; final ZoneSpecification spec = ZoneSpecification( print: (_, __, ___, String message) { @@ -297,6 +353,11 @@ Future> runCapturingPrint( rethrow; } errorHandler(e); + } on Exception catch (e) { + if (exceptionHandler == null) { + rethrow; + } + exceptionHandler(e); } return prints; @@ -344,10 +405,10 @@ class RecordingProcessRunner extends ProcessRunner { final io.Process? process = _getProcessToReturn(executable); final List? processStdout = await process?.stdout.transform(stdoutEncoding.decoder).toList(); - final String stdout = processStdout?.join('') ?? ''; + final String stdout = processStdout?.join() ?? ''; final List? processStderr = await process?.stderr.transform(stderrEncoding.decoder).toList(); - final String stderr = processStderr?.join('') ?? ''; + final String stderr = processStderr?.join() ?? ''; final io.ProcessResult result = process == null ? io.ProcessResult(1, 0, '', '') @@ -400,8 +461,7 @@ class ProcessCall { } @override - int get hashCode => - (executable.hashCode) ^ (args.hashCode) ^ (workingDir?.hashCode ?? 0); + int get hashCode => Object.hash(executable, args, workingDir); @override String toString() { diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 5a5a0a108a32..d485d81ceaf2 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -16,7 +16,7 @@ import 'package:mockito/mockito.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; -import 'common/plugin_command_test.mocks.dart'; +import 'common/package_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; @@ -44,7 +44,7 @@ class MockProcessResult extends Mock implements io.ProcessResult {} void main() { const String indentation = ' '; - group('$VersionCheckCommand', () { + group('VersionCheckCommand', () { late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; @@ -298,35 +298,25 @@ void main() { ])); }); - test('allows breaking changes to platform interfaces with explanation', + 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 File changeDescriptionFile = - fileSystem.file('change_description.txt'); - changeDescriptionFile.writeAsStringSync(''' -Some general PR description -## Breaking change justification - -This is necessary because of X, Y, and Z - -## Another section'''); final List output = await runCapturingPrint(runner, [ 'version-check', '--base-sha=main', - '--change-description-file=${changeDescriptionFile.path}' + '--pr-labels=some label,override: allow breaking change,another-label' ]); expect( output, containsAllInOrder([ contains('Allowing breaking change to plugin_platform_interface ' - 'due to "## Breaking change justification" in the change ' - 'description.'), + 'due to the "override: allow breaking change" label.'), contains('Ran for 1 package(s) (1 with warnings)'), ]), ); @@ -342,42 +332,6 @@ This is necessary because of X, Y, and Z ])); }); - test('throws if a nonexistent change description file is specified', - () 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', - '--change-description-file=a_missing_file.txt' - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No such file: a_missing_file.txt'), - ]), - ); - 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, @@ -414,14 +368,14 @@ This is necessary because of X, Y, and Z test('Allow empty lines in front of the first version in CHANGELOG', () async { const String version = '1.0.1'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: version); const String changelog = ''' ## $version * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main']); expect( @@ -433,13 +387,13 @@ This is necessary because of X, Y, and Z }); test('Throws if versions in changelog and pubspec do not match', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.1'); const String changelog = ''' ## 1.0.2 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); Error? commandError; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], @@ -458,14 +412,14 @@ This is necessary because of X, Y, and Z test('Success if CHANGELOG and pubspec versions match', () async { const String version = '1.0.1'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: version); const String changelog = ''' ## $version * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main']); expect( @@ -479,7 +433,7 @@ This is necessary because of X, Y, and Z test( 'Fail if pubspec version only matches an older version listed in CHANGELOG', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' @@ -488,7 +442,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); bool hasError = false; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], @@ -509,7 +463,7 @@ This is necessary because of X, Y, and Z test('Allow NEXT as a placeholder for gathering CHANGELOG entries', () async { const String version = '1.0.0'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: version); const String changelog = ''' @@ -518,7 +472,7 @@ This is necessary because of X, Y, and Z ## $version * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -536,7 +490,7 @@ This is necessary because of X, Y, and Z test('Fail if NEXT appears after a version', () async { const String version = '1.0.1'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: version); const String changelog = ''' @@ -547,7 +501,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); bool hasError = false; final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=main', '--against-pub'], @@ -561,7 +515,7 @@ This is necessary because of X, Y, and Z output, containsAllInOrder([ contains('When bumping the version for release, the NEXT section ' - 'should be incorporated into the new version\'s release notes.') + "should be incorporated into the new version's release notes.") ]), ); }); @@ -569,7 +523,7 @@ This is necessary because of X, Y, and Z test('Fail if NEXT is left in the CHANGELOG when adding a version bump', () async { const String version = '1.0.1'; - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: version); const String changelog = ''' @@ -580,7 +534,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); bool hasError = false; final List output = await runCapturingPrint( @@ -595,15 +549,15 @@ This is necessary because of X, Y, and Z output, containsAllInOrder([ contains('When bumping the version for release, the NEXT section ' - 'should be incorporated into the new version\'s release notes.'), + "should be incorporated into the new version's release notes."), contains('plugin:\n' ' CHANGELOG.md failed validation.'), ]), ); }); - test('Fail if the version changes without replacing NEXT', () async { - final Directory pluginDirectory = + test('fails if the version increases without replacing NEXT', () async { + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.1'); const String changelog = ''' @@ -612,7 +566,7 @@ This is necessary because of X, Y, and Z ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); bool hasError = false; final List output = await runCapturingPrint( @@ -627,7 +581,34 @@ This is necessary because of X, Y, and Z output, containsAllInOrder([ contains('When bumping the version for release, the NEXT section ' - 'should be incorporated into the new version\'s release notes.') + "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.'), ]), ); }); @@ -635,7 +616,7 @@ This is necessary because of X, Y, and Z test( 'fails gracefully if the version headers are not found due to using the wrong style', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' @@ -644,7 +625,7 @@ This is necessary because of X, Y, and Z # 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -670,14 +651,14 @@ This is necessary because of X, Y, and Z }); test('fails gracefully if the version is unparseable', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## Alpha * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -700,8 +681,7 @@ This is necessary because of X, Y, and Z }); group('missing change detection', () { - Future> _runWithMissingChangeDetection( - List extraArgs, + Future> runWithMissingChangeDetection(List extraArgs, {void Function(Error error)? errorHandler}) async { return runCapturingPrint( runner, @@ -715,14 +695,14 @@ This is necessary because of X, Y, and Z } test('passes for unchanged packages', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -731,7 +711,7 @@ This is necessary because of X, Y, and Z ]; final List output = - await _runWithMissingChangeDetection([]); + await runWithMissingChangeDetection([]); expect( output, @@ -744,14 +724,14 @@ This is necessary because of X, Y, and Z test( 'fails if a version change is missing from a change that does not ' 'pass the exemption check', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -762,7 +742,7 @@ packages/plugin/lib/plugin.dart ]; Error? commandError; - final List output = await _runWithMissingChangeDetection( + final List output = await runWithMissingChangeDetection( [], errorHandler: (Error e) { commandError = e; }); @@ -779,14 +759,14 @@ packages/plugin/lib/plugin.dart }); test('passes version change requirement when version changes', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.1'); const String changelog = ''' ## 1.0.1 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -799,7 +779,7 @@ packages/plugin/pubspec.yaml ]; final List output = - await _runWithMissingChangeDetection([]); + await runWithMissingChangeDetection([]); expect( output, @@ -810,14 +790,14 @@ packages/plugin/pubspec.yaml }); test('version change check ignores files outside the package', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -829,7 +809,7 @@ tool/plugin/lib/plugin.dart ]; final List output = - await _runWithMissingChangeDetection([]); + await runWithMissingChangeDetection([]); expect( output, @@ -840,14 +820,14 @@ tool/plugin/lib/plugin.dart }); test('allows missing version change for exempt changes', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -862,7 +842,7 @@ packages/plugin/CHANGELOG.md ]; final List output = - await _runWithMissingChangeDetection([]); + await runWithMissingChangeDetection([]); expect( output, @@ -872,15 +852,15 @@ packages/plugin/CHANGELOG.md ); }); - test('allows missing version change with justification', () async { - final Directory pluginDirectory = + 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. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -892,36 +872,29 @@ packages/plugin/pubspec.yaml '''), ]; - final File changeDescriptionFile = - fileSystem.file('change_description.txt'); - changeDescriptionFile.writeAsStringSync(''' -Some general PR description - -No version change: Code change is only to implementation comments. -'''); final List output = - await _runWithMissingChangeDetection([ - '--change-description-file=${changeDescriptionFile.path}' + await runWithMissingChangeDetection([ + '--pr-labels=some label,override: no versioning needed,another-label' ]); expect( output, containsAllInOrder([ - contains('Ignoring lack of version change due to ' - '"No version change:" in the change description.'), + contains('Ignoring lack of version change due to the ' + '"override: no versioning needed" label.'), ]), ); }); test('fails if a CHANGELOG change is missing', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -932,7 +905,7 @@ packages/plugin/example/lib/foo.dart ]; Error? commandError; - final List output = await _runWithMissingChangeDetection( + final List output = await runWithMissingChangeDetection( [], errorHandler: (Error e) { commandError = e; }); @@ -949,14 +922,14 @@ packages/plugin/example/lib/foo.dart }); test('passes CHANGELOG check when the CHANGELOG is changed', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -968,7 +941,7 @@ packages/plugin/CHANGELOG.md ]; final List output = - await _runWithMissingChangeDetection([]); + await runWithMissingChangeDetection([]); expect( output, @@ -980,14 +953,14 @@ packages/plugin/CHANGELOG.md test('fails CHANGELOG check if only another package CHANGELOG chages', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -999,7 +972,7 @@ packages/another_plugin/CHANGELOG.md ]; Error? commandError; - final List output = await _runWithMissingChangeDetection( + final List output = await runWithMissingChangeDetection( [], errorHandler: (Error e) { commandError = e; }); @@ -1014,14 +987,14 @@ packages/another_plugin/CHANGELOG.md }); test('allows missing CHANGELOG change with justification', () async { - final Directory pluginDirectory = + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, version: '1.0.0'); const String changelog = ''' ## 1.0.0 * Some changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); + plugin.changelogFile.writeAsStringSync(changelog); processRunner.mockProcessesForExecutable['git-show'] = [ MockProcess(stdout: 'version: 1.0.0'), ]; @@ -1031,23 +1004,89 @@ packages/plugin/example/lib/foo.dart '''), ]; - final File changeDescriptionFile = - fileSystem.file('change_description.txt'); - changeDescriptionFile.writeAsStringSync(''' -Some general PR description - -No CHANGELOG change: Code change is only to implementation comments. -'''); final List output = - await _runWithMissingChangeDetection([ - '--change-description-file=${changeDescriptionFile.path}' + await runWithMissingChangeDetection([ + '--pr-labels=some label,override: no changelog needed,another-label' ]); expect( output, containsAllInOrder([ - contains('Ignoring lack of CHANGELOG update due to ' - '"No CHANGELOG change:" in the change description.'), + 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'), ]), ); }); @@ -1151,6 +1190,186 @@ ${indentation}HTTP response: null ]), ); }); + + 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', () { diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart index 097d4d21cb19..418c695f295c 100644 --- a/script/tool/test/xcode_analyze_command_test.dart +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -82,13 +82,12 @@ void main() { }); test('runs for iOS plugin', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'xcode-analyze', @@ -124,6 +123,47 @@ void main() { ])); }); + 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: { @@ -184,14 +224,12 @@ void main() { }); test('runs for macOS plugin', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'xcode-analyze', @@ -221,6 +259,41 @@ void main() { ])); }); + 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: { @@ -251,15 +324,13 @@ void main() { group('combined', () { test('runs both iOS and macOS when supported', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformIOS: const PlatformDetails(PlatformSupport.inline), platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'xcode-analyze', @@ -311,14 +382,12 @@ void main() { }); test('runs only macOS for a macOS plugin', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'xcode-analyze', @@ -353,13 +422,12 @@ void main() { }); test('runs only iOS for a iOS plugin', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + platformIOS: const PlatformDetails(PlatformSupport.inline) + }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'xcode-analyze', diff --git a/script/tool_runner.sh b/script/tool_runner.sh index 66181543f2cf..221071550cc1 100755 --- a/script/tool_runner.sh +++ b/script/tool_runner.sh @@ -19,4 +19,5 @@ readonly TOOL_PATH="$REPO_DIR/script/tool/bin/flutter_plugin_tools.dart" # The tool expects to be run from the repo root. cd "$REPO_DIR" # Run from the in-tree source. -dart run "$TOOL_PATH" "$@" --packages-for-branch --log-timing $PLUGIN_SHARDING +# PACKAGE_SHARDING is (optionally) set from Cirrus. See .cirrus.yml +dart run "$TOOL_PATH" "$@" --packages-for-branch --log-timing $PACKAGE_SHARDING diff --git a/site-shared b/site-shared new file mode 160000 index 000000000000..142de133477b --- /dev/null +++ b/site-shared @@ -0,0 +1 @@ +Subproject commit 142de133477bdede1746f992e656c4b43c4c7442